@yuneta/gobj-ui 7.23.139 → 7.23.140

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.
@@ -911,6 +911,7 @@ var PRIVATE_DATA$21 = {
911
911
  navs: [],
912
912
  item_index: {},
913
913
  section_pos: {},
914
+ route_mru: [],
914
915
  sub_routes: {},
915
916
  event_handlers: {},
916
917
  hash_handler: null,
@@ -1619,6 +1620,7 @@ function navigate_to(gobj, route, depth, no_drain) {
1619
1620
  stage.active_route = matched_route;
1620
1621
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "current_route", route);
1621
1622
  remember_position(gobj, route);
1623
+ remember_route(gobj, route);
1622
1624
  update_secondary_nav_visibility(gobj, entry);
1623
1625
  close_all_drawers(gobj);
1624
1626
  close_toolbar_dropdown(gobj);
@@ -2645,6 +2647,40 @@ function remembered_position(gobj, route) {
2645
2647
  return last;
2646
2648
  }
2647
2649
  /************************************************************
2650
+ * Every route the shell lands on, newest last, a route once.
2651
+ * Bounded: it answers "where was the reader last under X", and
2652
+ * a page that has been open all day has no business growing it.
2653
+ ************************************************************/
2654
+ var ROUTE_MRU_MAX = 64;
2655
+ function remember_route(gobj, route) {
2656
+ let mru = gobj.priv.route_mru;
2657
+ let i = mru.indexOf(route);
2658
+ if (i >= 0) mru.splice(i, 1);
2659
+ mru.push(route);
2660
+ if (mru.length > ROUTE_MRU_MAX) mru.splice(0, mru.length - ROUTE_MRU_MAX);
2661
+ }
2662
+ /************************************************************
2663
+ * Where the reader last was UNDER `route`: the most recent
2664
+ * visited route that is `route` itself or lies below it, or
2665
+ * `route` when none does. A mirror of the url for the page, like
2666
+ * the section memory -- never stored, never applied on its own:
2667
+ * a control that wants to go back to a view AS IT WAS LEFT asks,
2668
+ * and navigates there itself.
2669
+ *
2670
+ * What it is for: a treedb graph's focus lives in the url
2671
+ * (`<graph>/<topic>`), so a button that opened the bare route
2672
+ * took the focus away every time it was used.
2673
+ ************************************************************/
2674
+ function yui_shell_last_route_under(shell_gobj, route) {
2675
+ let priv = shell_gobj && shell_gobj.priv;
2676
+ if (!priv || !(0, _yuneta_gobj_js.is_array)(priv.route_mru) || !route) return route;
2677
+ for (let i = priv.route_mru.length - 1; i >= 0; i--) {
2678
+ let r = priv.route_mru[i];
2679
+ if (r === route || r.indexOf(route + "/") === 0) return r;
2680
+ }
2681
+ return route;
2682
+ }
2683
+ /************************************************************
2648
2684
  * Resolve the shell that governs `gobj`: the nearest
2649
2685
  * C_YUI_SHELL ancestor, else the last shell created on the
2650
2686
  * page (apps have exactly one). Null when no shell exists —
@@ -21908,15 +21944,12 @@ function cmd_get_topic_data$1(gobj, cmd, kw, src) {
21908
21944
  ************************************************************/
21909
21945
  function build_ui$6(gobj) {
21910
21946
  let source_url = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "source_url") || "";
21911
- let routes = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "card_action_routes");
21912
- let graph_tpl = (0, _yuneta_gobj_js.is_object)(routes) && typeof routes.graph === "string" ? routes.graph : "";
21913
- let graph_href = /\/\{topic\}$/.test(graph_tpl) ? graph_tpl.replace(/\/\{topic\}$/, "") : "";
21947
+ let graph_href = graph_route_of(gobj);
21914
21948
  let graph_items = [];
21915
21949
  if (graph_href) graph_items.push([
21916
- "a",
21950
+ "button",
21917
21951
  {
21918
21952
  class: "button TREEDB_GRAPH_BTN",
21919
- href: graph_href,
21920
21953
  style: "margin-left:auto;",
21921
21954
  title: (0, i18next.t)("graph"),
21922
21955
  "aria-label": (0, i18next.t)("graph"),
@@ -21931,7 +21964,11 @@ function build_ui$6(gobj) {
21931
21964
  "span",
21932
21965
  { i18n: "graph" },
21933
21966
  "graph"
21934
- ]]
21967
+ ]],
21968
+ { click: (evt) => {
21969
+ evt.stopPropagation();
21970
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_OPEN_GRAPH", {}, gobj);
21971
+ } }
21935
21972
  ]);
21936
21973
  let $container = (0, _yuneta_gobj_js.createElement2)([
21937
21974
  "div",
@@ -22092,6 +22129,17 @@ function build_ui$6(gobj) {
22092
22129
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_cards_landing")) show_topics_landing(gobj);
22093
22130
  }
22094
22131
  /************************************************************
22132
+ * The graph's route with no topic: the host's card template
22133
+ * `card_action_routes.graph` without its trailing `/{topic}`,
22134
+ * as a ROUTE (no `#`). "" when the host gives no such template.
22135
+ ************************************************************/
22136
+ function graph_route_of(gobj) {
22137
+ let routes = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "card_action_routes");
22138
+ let tpl = (0, _yuneta_gobj_js.is_object)(routes) && typeof routes.graph === "string" ? routes.graph : "";
22139
+ if (!/\/\{topic\}$/.test(tpl)) return "";
22140
+ return tpl.replace(/\/\{topic\}$/, "").replace(/^#/, "");
22141
+ }
22142
+ /************************************************************
22095
22143
  * Is the backend session live? The Raw JSON viewer issues a remote
22096
22144
  * print-tranger, so it only makes sense with a session — the remote
22097
22145
  * (C_IEVENT_CLI) is in ST_SESSION exactly while connected.
@@ -23460,6 +23508,26 @@ function ac_open_json$1(gobj, event, kw, src) {
23460
23508
  return 0;
23461
23509
  }
23462
23510
  /********************************************
23511
+ * Open the graph where the reader left it: its last visited
23512
+ * route under the graph's base -- `<graph>/<topic>` when a topic
23513
+ * was focused there -- or the bare base when the graph was never
23514
+ * opened in this page. A user move: pushed, so Back returns.
23515
+ ********************************************/
23516
+ function ac_open_graph(gobj, event, kw, src) {
23517
+ let base = graph_route_of(gobj);
23518
+ if (!base) {
23519
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: open graph with no graph route`);
23520
+ return -1;
23521
+ }
23522
+ let shell = yui_shell_of(gobj);
23523
+ if (!shell) {
23524
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: open graph with no shell`);
23525
+ return -1;
23526
+ }
23527
+ yui_shell_navigate(shell, yui_shell_last_route_under(shell, base), { push: true });
23528
+ return 0;
23529
+ }
23530
+ /********************************************
23463
23531
  * The viewer asked to load a collapsed subtree (tranger mode only):
23464
23532
  * re-issue print-tranger for that path.
23465
23533
  ********************************************/
@@ -23558,6 +23626,11 @@ function create_gclass$6(gclass_name) {
23558
23626
  ac_open_json$1,
23559
23627
  null
23560
23628
  ],
23629
+ [
23630
+ "EV_OPEN_GRAPH",
23631
+ ac_open_graph,
23632
+ null
23633
+ ],
23561
23634
  [
23562
23635
  "EV_EXPAND_PATH",
23563
23636
  ac_json_expand_path$1,
@@ -23622,6 +23695,7 @@ function create_gclass$6(gclass_name) {
23622
23695
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
23623
23696
  ["EV_REFRESH_TOPIC", 0],
23624
23697
  ["EV_OPEN_JSON", 0],
23698
+ ["EV_OPEN_GRAPH", 0],
23625
23699
  ["EV_EXPAND_PATH", 0],
23626
23700
  ["EV_JSON_CLOSED", 0],
23627
23701
  ["EV_SHOW", 0],
@@ -906,6 +906,7 @@ var PRIVATE_DATA$21 = {
906
906
  navs: [],
907
907
  item_index: {},
908
908
  section_pos: {},
909
+ route_mru: [],
909
910
  sub_routes: {},
910
911
  event_handlers: {},
911
912
  hash_handler: null,
@@ -1614,6 +1615,7 @@ function navigate_to(gobj, route, depth, no_drain) {
1614
1615
  stage.active_route = matched_route;
1615
1616
  gobj_write_attr(gobj, "current_route", route);
1616
1617
  remember_position(gobj, route);
1618
+ remember_route(gobj, route);
1617
1619
  update_secondary_nav_visibility(gobj, entry);
1618
1620
  close_all_drawers(gobj);
1619
1621
  close_toolbar_dropdown(gobj);
@@ -2640,6 +2642,40 @@ function remembered_position(gobj, route) {
2640
2642
  return last;
2641
2643
  }
2642
2644
  /************************************************************
2645
+ * Every route the shell lands on, newest last, a route once.
2646
+ * Bounded: it answers "where was the reader last under X", and
2647
+ * a page that has been open all day has no business growing it.
2648
+ ************************************************************/
2649
+ var ROUTE_MRU_MAX = 64;
2650
+ function remember_route(gobj, route) {
2651
+ let mru = gobj.priv.route_mru;
2652
+ let i = mru.indexOf(route);
2653
+ if (i >= 0) mru.splice(i, 1);
2654
+ mru.push(route);
2655
+ if (mru.length > ROUTE_MRU_MAX) mru.splice(0, mru.length - ROUTE_MRU_MAX);
2656
+ }
2657
+ /************************************************************
2658
+ * Where the reader last was UNDER `route`: the most recent
2659
+ * visited route that is `route` itself or lies below it, or
2660
+ * `route` when none does. A mirror of the url for the page, like
2661
+ * the section memory -- never stored, never applied on its own:
2662
+ * a control that wants to go back to a view AS IT WAS LEFT asks,
2663
+ * and navigates there itself.
2664
+ *
2665
+ * What it is for: a treedb graph's focus lives in the url
2666
+ * (`<graph>/<topic>`), so a button that opened the bare route
2667
+ * took the focus away every time it was used.
2668
+ ************************************************************/
2669
+ function yui_shell_last_route_under(shell_gobj, route) {
2670
+ let priv = shell_gobj && shell_gobj.priv;
2671
+ if (!priv || !is_array(priv.route_mru) || !route) return route;
2672
+ for (let i = priv.route_mru.length - 1; i >= 0; i--) {
2673
+ let r = priv.route_mru[i];
2674
+ if (r === route || r.indexOf(route + "/") === 0) return r;
2675
+ }
2676
+ return route;
2677
+ }
2678
+ /************************************************************
2643
2679
  * Resolve the shell that governs `gobj`: the nearest
2644
2680
  * C_YUI_SHELL ancestor, else the last shell created on the
2645
2681
  * page (apps have exactly one). Null when no shell exists —
@@ -21903,15 +21939,12 @@ function cmd_get_topic_data$1(gobj, cmd, kw, src) {
21903
21939
  ************************************************************/
21904
21940
  function build_ui$6(gobj) {
21905
21941
  let source_url = gobj_read_str_attr(gobj, "source_url") || "";
21906
- let routes = gobj_read_attr(gobj, "card_action_routes");
21907
- let graph_tpl = is_object(routes) && typeof routes.graph === "string" ? routes.graph : "";
21908
- let graph_href = /\/\{topic\}$/.test(graph_tpl) ? graph_tpl.replace(/\/\{topic\}$/, "") : "";
21942
+ let graph_href = graph_route_of(gobj);
21909
21943
  let graph_items = [];
21910
21944
  if (graph_href) graph_items.push([
21911
- "a",
21945
+ "button",
21912
21946
  {
21913
21947
  class: "button TREEDB_GRAPH_BTN",
21914
- href: graph_href,
21915
21948
  style: "margin-left:auto;",
21916
21949
  title: t("graph"),
21917
21950
  "aria-label": t("graph"),
@@ -21926,7 +21959,11 @@ function build_ui$6(gobj) {
21926
21959
  "span",
21927
21960
  { i18n: "graph" },
21928
21961
  "graph"
21929
- ]]
21962
+ ]],
21963
+ { click: (evt) => {
21964
+ evt.stopPropagation();
21965
+ gobj_send_event(gobj, "EV_OPEN_GRAPH", {}, gobj);
21966
+ } }
21930
21967
  ]);
21931
21968
  let $container = createElement2([
21932
21969
  "div",
@@ -22087,6 +22124,17 @@ function build_ui$6(gobj) {
22087
22124
  if (gobj_read_bool_attr(gobj, "with_cards_landing")) show_topics_landing(gobj);
22088
22125
  }
22089
22126
  /************************************************************
22127
+ * The graph's route with no topic: the host's card template
22128
+ * `card_action_routes.graph` without its trailing `/{topic}`,
22129
+ * as a ROUTE (no `#`). "" when the host gives no such template.
22130
+ ************************************************************/
22131
+ function graph_route_of(gobj) {
22132
+ let routes = gobj_read_attr(gobj, "card_action_routes");
22133
+ let tpl = is_object(routes) && typeof routes.graph === "string" ? routes.graph : "";
22134
+ if (!/\/\{topic\}$/.test(tpl)) return "";
22135
+ return tpl.replace(/\/\{topic\}$/, "").replace(/^#/, "");
22136
+ }
22137
+ /************************************************************
22090
22138
  * Is the backend session live? The Raw JSON viewer issues a remote
22091
22139
  * print-tranger, so it only makes sense with a session — the remote
22092
22140
  * (C_IEVENT_CLI) is in ST_SESSION exactly while connected.
@@ -23455,6 +23503,26 @@ function ac_open_json$1(gobj, event, kw, src) {
23455
23503
  return 0;
23456
23504
  }
23457
23505
  /********************************************
23506
+ * Open the graph where the reader left it: its last visited
23507
+ * route under the graph's base -- `<graph>/<topic>` when a topic
23508
+ * was focused there -- or the bare base when the graph was never
23509
+ * opened in this page. A user move: pushed, so Back returns.
23510
+ ********************************************/
23511
+ function ac_open_graph(gobj, event, kw, src) {
23512
+ let base = graph_route_of(gobj);
23513
+ if (!base) {
23514
+ log_error(`${gobj_short_name(gobj)}: open graph with no graph route`);
23515
+ return -1;
23516
+ }
23517
+ let shell = yui_shell_of(gobj);
23518
+ if (!shell) {
23519
+ log_error(`${gobj_short_name(gobj)}: open graph with no shell`);
23520
+ return -1;
23521
+ }
23522
+ yui_shell_navigate(shell, yui_shell_last_route_under(shell, base), { push: true });
23523
+ return 0;
23524
+ }
23525
+ /********************************************
23458
23526
  * The viewer asked to load a collapsed subtree (tranger mode only):
23459
23527
  * re-issue print-tranger for that path.
23460
23528
  ********************************************/
@@ -23553,6 +23621,11 @@ function create_gclass$6(gclass_name) {
23553
23621
  ac_open_json$1,
23554
23622
  null
23555
23623
  ],
23624
+ [
23625
+ "EV_OPEN_GRAPH",
23626
+ ac_open_graph,
23627
+ null
23628
+ ],
23556
23629
  [
23557
23630
  "EV_EXPAND_PATH",
23558
23631
  ac_json_expand_path$1,
@@ -23617,6 +23690,7 @@ function create_gclass$6(gclass_name) {
23617
23690
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
23618
23691
  ["EV_REFRESH_TOPIC", 0],
23619
23692
  ["EV_OPEN_JSON", 0],
23693
+ ["EV_OPEN_GRAPH", 0],
23620
23694
  ["EV_EXPAND_PATH", 0],
23621
23695
  ["EV_JSON_CLOSED", 0],
23622
23696
  ["EV_SHOW", 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.139",
3
+ "version": "7.23.140",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_node.js CHANGED
@@ -1324,6 +1324,12 @@ function ac_add_node(gobj, event, kw, src)
1324
1324
  if(spec.link) {
1325
1325
  kw_child.link = spec.link;
1326
1326
  }
1327
+ /* A declared attr of this gclass that the spec could not reach: a
1328
+ * node whose children are workspaces (a treedb: its topics, its
1329
+ * graph) is declared in the config like every other one. */
1330
+ if(spec.remember_position) {
1331
+ kw_child.remember_position = true;
1332
+ }
1327
1333
  let child = gobj_create_pure_child(spec.id, GCLASS_NAME, kw_child, gobj);
1328
1334
 
1329
1335
  let index = (typeof kw.index === "number") ? kw.index : priv.children.length;
@@ -117,6 +117,10 @@ let PRIVATE_DATA = {
117
117
  /* Section route -> last position visited inside it. Mirrors the
118
118
  * url for the duration of the page; see remember_position(). */
119
119
  section_pos: {},
120
+ /* The routes visited, newest last, bounded -- for "where did the
121
+ * reader leave this subtree" (yui_shell_last_route_under). Same
122
+ * nature as section_pos: a mirror of the url, for the page. */
123
+ route_mru: [],
120
124
  /* Sub-route contributor registry (ROUTING.md): a mounted view
121
125
  * declares the deep, view-owned children of its base route
122
126
  * (topics, /info, /schema, focus topics — subpaths that are NOT
@@ -1465,6 +1469,7 @@ function navigate_to(gobj, route, depth, no_drain)
1465
1469
  stage.active_route = matched_route;
1466
1470
  gobj_write_attr(gobj, "current_route", route);
1467
1471
  remember_position(gobj, route);
1472
+ remember_route(gobj, route);
1468
1473
 
1469
1474
  /* Show/hide secondary navs according to parent item */
1470
1475
  update_secondary_nav_visibility(gobj, entry);
@@ -3187,6 +3192,54 @@ function remembered_position(gobj, route)
3187
3192
  return last;
3188
3193
  }
3189
3194
 
3195
+ /************************************************************
3196
+ * Every route the shell lands on, newest last, a route once.
3197
+ * Bounded: it answers "where was the reader last under X", and
3198
+ * a page that has been open all day has no business growing it.
3199
+ ************************************************************/
3200
+ const ROUTE_MRU_MAX = 64;
3201
+
3202
+ function remember_route(gobj, route)
3203
+ {
3204
+ let priv = gobj.priv;
3205
+ let mru = priv.route_mru;
3206
+ let i = mru.indexOf(route);
3207
+ if(i >= 0) {
3208
+ mru.splice(i, 1);
3209
+ }
3210
+ mru.push(route);
3211
+ if(mru.length > ROUTE_MRU_MAX) {
3212
+ mru.splice(0, mru.length - ROUTE_MRU_MAX);
3213
+ }
3214
+ }
3215
+
3216
+ /************************************************************
3217
+ * Where the reader last was UNDER `route`: the most recent
3218
+ * visited route that is `route` itself or lies below it, or
3219
+ * `route` when none does. A mirror of the url for the page, like
3220
+ * the section memory -- never stored, never applied on its own:
3221
+ * a control that wants to go back to a view AS IT WAS LEFT asks,
3222
+ * and navigates there itself.
3223
+ *
3224
+ * What it is for: a treedb graph's focus lives in the url
3225
+ * (`<graph>/<topic>`), so a button that opened the bare route
3226
+ * took the focus away every time it was used.
3227
+ ************************************************************/
3228
+ function yui_shell_last_route_under(shell_gobj, route)
3229
+ {
3230
+ let priv = shell_gobj && shell_gobj.priv;
3231
+ if(!priv || !is_array(priv.route_mru) || !route) {
3232
+ return route;
3233
+ }
3234
+ for(let i = priv.route_mru.length - 1; i >= 0; i--) {
3235
+ let r = priv.route_mru[i];
3236
+ if(r === route || r.indexOf(route + "/") === 0) {
3237
+ return r;
3238
+ }
3239
+ }
3240
+ return route;
3241
+ }
3242
+
3190
3243
  /************************************************************
3191
3244
  * Resolve the shell that governs `gobj`: the nearest
3192
3245
  * C_YUI_SHELL ancestor, else the last shell created on the
@@ -3609,6 +3662,7 @@ export {
3609
3662
  yui_shell_of,
3610
3663
  yui_shell_zone,
3611
3664
  yui_shell_navigate,
3665
+ yui_shell_last_route_under,
3612
3666
  yui_shell_nav_map,
3613
3667
  yui_shell_set_sub_routes,
3614
3668
  yui_shell_register_event_handler,
@@ -58,7 +58,12 @@ import {
58
58
  import "./c_yui_treedb_topics.css";
59
59
 
60
60
  import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
61
- import {yui_shell_of, yui_shell_set_sub_routes} from "./c_yui_shell.js";
61
+ import {
62
+ yui_shell_of,
63
+ yui_shell_set_sub_routes,
64
+ yui_shell_navigate,
65
+ yui_shell_last_route_under,
66
+ } from "./c_yui_shell.js";
62
67
  import {nodes_answer} from "./nodes_answer.js";
63
68
  import {yui_toolbar_icon} from "./yui_toolbar.js";
64
69
 
@@ -254,26 +259,28 @@ function build_ui(gobj)
254
259
  {
255
260
  let source_url = gobj_read_str_attr(gobj, "source_url") || "";
256
261
 
257
- /* The whole treedb as a graph, with NO topic focused. A card's graph
258
- * icon lands on `<graph route>/<topic>`, and that segment is a
259
- * FOCUS: going through a card was the only way in, and it always
260
- * arrived with that topic highlighted. The route is the card's
261
- * template without its `/{topic}`; a host whose template does not
262
- * end in it, or gives none, gets no button. */
263
- let routes = gobj_read_attr(gobj, "card_action_routes");
264
- let graph_tpl = is_object(routes) && typeof routes.graph === "string"? routes.graph : "";
265
- let graph_href = /\/\{topic\}$/.test(graph_tpl)? graph_tpl.replace(/\/\{topic\}$/, "") : "";
262
+ /* The treedb as a graph, AS THE READER LEFT IT. The route is the
263
+ * card template without its `/{topic}`; a host whose template does
264
+ * not end in it, or gives none, gets no button. Not a fixed link:
265
+ * the graph's focus lives in its url (`<graph>/<topic>`), so the
266
+ * bare route took it away every time -- the click asks the shell
267
+ * where the reader last was under that route (ac_open_graph). */
268
+ let graph_href = graph_route_of(gobj);
266
269
  let graph_items = [];
267
270
  if(graph_href) {
268
271
  graph_items.push(
269
- ['a', {class: 'button TREEDB_GRAPH_BTN',
270
- href: graph_href,
271
- style: 'margin-left:auto;',
272
- title: t('graph'), 'aria-label': t('graph'),
273
- 'data-i18n-title': 'graph', 'data-i18n-aria-label': 'graph'}, [
272
+ ['button', {class: 'button TREEDB_GRAPH_BTN',
273
+ style: 'margin-left:auto;',
274
+ title: t('graph'), 'aria-label': t('graph'),
275
+ 'data-i18n-title': 'graph', 'data-i18n-aria-label': 'graph'}, [
274
276
  ['span', {class: 'icon'}, [yui_toolbar_icon('yi-hexagon-nodes')]],
275
277
  ['span', {i18n: 'graph'}, 'graph']
276
- ]]
278
+ ], {
279
+ click: (evt) => {
280
+ evt.stopPropagation();
281
+ gobj_send_event(gobj, "EV_OPEN_GRAPH", {}, gobj);
282
+ }
283
+ }]
277
284
  );
278
285
  }
279
286
 
@@ -389,6 +396,21 @@ function build_ui(gobj)
389
396
  }
390
397
  }
391
398
 
399
+ /************************************************************
400
+ * The graph's route with no topic: the host's card template
401
+ * `card_action_routes.graph` without its trailing `/{topic}`,
402
+ * as a ROUTE (no `#`). "" when the host gives no such template.
403
+ ************************************************************/
404
+ function graph_route_of(gobj)
405
+ {
406
+ let routes = gobj_read_attr(gobj, "card_action_routes");
407
+ let tpl = (is_object(routes) && typeof routes.graph === "string")? routes.graph : "";
408
+ if(!/\/\{topic\}$/.test(tpl)) {
409
+ return "";
410
+ }
411
+ return tpl.replace(/\/\{topic\}$/, "").replace(/^#/, "");
412
+ }
413
+
392
414
  /************************************************************
393
415
  * Is the backend session live? The Raw JSON viewer issues a remote
394
416
  * print-tranger, so it only makes sense with a session — the remote
@@ -2476,6 +2498,28 @@ function ac_open_json(gobj, event, kw, src)
2476
2498
  return 0;
2477
2499
  }
2478
2500
 
2501
+ /********************************************
2502
+ * Open the graph where the reader left it: its last visited
2503
+ * route under the graph's base -- `<graph>/<topic>` when a topic
2504
+ * was focused there -- or the bare base when the graph was never
2505
+ * opened in this page. A user move: pushed, so Back returns.
2506
+ ********************************************/
2507
+ function ac_open_graph(gobj, event, kw, src)
2508
+ {
2509
+ let base = graph_route_of(gobj);
2510
+ if(!base) {
2511
+ log_error(`${gobj_short_name(gobj)}: open graph with no graph route`);
2512
+ return -1;
2513
+ }
2514
+ let shell = yui_shell_of(gobj);
2515
+ if(!shell) {
2516
+ log_error(`${gobj_short_name(gobj)}: open graph with no shell`);
2517
+ return -1;
2518
+ }
2519
+ yui_shell_navigate(shell, yui_shell_last_route_under(shell, base), {push: true});
2520
+ return 0;
2521
+ }
2522
+
2479
2523
  /********************************************
2480
2524
  * The viewer asked to load a collapsed subtree (tranger mode only):
2481
2525
  * re-issue print-tranger for that path.
@@ -2556,6 +2600,7 @@ function create_gclass(gclass_name)
2556
2600
  ["EV_DELETE_RECORD", ac_delete_record, null],
2557
2601
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2558
2602
  ["EV_OPEN_JSON", ac_open_json, null],
2603
+ ["EV_OPEN_GRAPH", ac_open_graph, null],
2559
2604
  ["EV_EXPAND_PATH", ac_json_expand_path, null],
2560
2605
  ["EV_JSON_CLOSED", ac_json_closed, null],
2561
2606
  ["EV_SHOW", ac_show, null],
@@ -2586,6 +2631,7 @@ function create_gclass(gclass_name)
2586
2631
  event_flag_t.EVF_NO_WARN_SUBS],
2587
2632
  ["EV_REFRESH_TOPIC", 0],
2588
2633
  ["EV_OPEN_JSON", 0],
2634
+ ["EV_OPEN_GRAPH", 0],
2589
2635
  ["EV_EXPAND_PATH", 0],
2590
2636
  ["EV_JSON_CLOSED", 0],
2591
2637
  ["EV_SHOW", 0],