@yuneta/gobj-ui 7.23.18 → 7.23.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.18",
3
+ "version": "7.23.19",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -85,6 +85,7 @@ import {
85
85
 
86
86
  import {node_label} from "./treedb_node_label.js";
87
87
  import {delete_impact} from "./delete_impact.js";
88
+ import {yui_graph_center_on} from "./yui_graph_camera.js";
88
89
 
89
90
  import {
90
91
  BaseLayout,
@@ -295,6 +296,11 @@ SDATA_END()
295
296
  ];
296
297
 
297
298
  let PRIVATE_DATA = {
299
+ /*---------------- camera anchor ----------------*/
300
+ anchor_id: "", // the node the camera keeps in the middle
301
+ anchor_state: "off", // "off" | "arming" | "on"
302
+ last_zoom: 1, // tells a wheel zoom from a drag in aftertransform
303
+
298
304
  _xy: 100,
299
305
  _edge_seq: 0,
300
306
  _history_paused: false,
@@ -833,6 +839,27 @@ function configure_events(gobj)
833
839
  update_node_icon_position(gobj);
834
840
  update_link_icon_position(gobj);
835
841
  update_zoom_readout(gobj);
842
+
843
+ /*
844
+ * A ZOOM re-centres on the anchor; a PAN does not.
845
+ * `aftertransform` fires for both, and an anchor that also
846
+ * answered a drag would make the graph impossible to move
847
+ * while it is set. The zoom LEVEL is what tells them apart:
848
+ * it changes on a wheel notch and never on a drag.
849
+ */
850
+ let z = priv.last_zoom;
851
+ try {
852
+ z = priv.graph.getZoom();
853
+ } catch(e) {
854
+ return;
855
+ }
856
+ if(z === priv.last_zoom) {
857
+ return;
858
+ }
859
+ priv.last_zoom = z;
860
+ if(priv.anchor_state === "on" && priv.anchor_id) {
861
+ yui_graph_center_on(priv.graph, priv.anchor_id);
862
+ }
836
863
  });
837
864
 
838
865
  // Re-render G6 toolbars when language changes
@@ -1236,6 +1263,19 @@ function configure_toolbar(gobj)
1236
1263
  * Actual size is WRITTEN in every editor that offers
1237
1264
  * it, never drawn: there is no glyph for it. */
1238
1265
  { text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size') },
1266
+ /* Pick one node and every zoom leaves it in the
1267
+ * middle. The pressed classes are baked in HERE and
1268
+ * not set afterwards, for the same reason the
1269
+ * selection-mode button bakes its own: this list is
1270
+ * rebuilt asynchronously, and a button told after
1271
+ * the rebuild came back OFF. */
1272
+ {
1273
+ id: 'g6-icon-anchor', value: 'anchor',
1274
+ className: 'EV_TOGGLE_ANCHOR' +
1275
+ (priv.anchor_state !== "off"? ' pressed_state' : '') +
1276
+ (priv.anchor_state === "arming"? ' color_pending_state' : ''),
1277
+ title: anchor_title(priv.anchor_state)
1278
+ },
1239
1279
  ];
1240
1280
 
1241
1281
  /* Only in edition, because that is where a selection
@@ -1288,6 +1328,9 @@ function configure_toolbar(gobj)
1288
1328
  case 'reset':
1289
1329
  gobj_send_event(gobj, "EV_ZOOM_RESET", {}, gobj);
1290
1330
  break;
1331
+ case 'anchor':
1332
+ gobj_send_event(gobj, "EV_TOGGLE_ANCHOR", {}, gobj);
1333
+ break;
1291
1334
  case 'auto-fit':
1292
1335
  gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
1293
1336
  break;
@@ -2839,7 +2882,7 @@ async function graph_zoom_in(gobj)
2839
2882
  let graph = priv.graph;
2840
2883
 
2841
2884
  const z = graph.getZoom();
2842
- await graph.zoomTo(z * 1.1);
2885
+ after_camera(gobj, graph.zoomTo(z * 1.1));
2843
2886
  }
2844
2887
 
2845
2888
  async function graph_zoom_out(gobj)
@@ -2848,7 +2891,7 @@ async function graph_zoom_out(gobj)
2848
2891
  let graph = priv.graph;
2849
2892
 
2850
2893
  const z = graph.getZoom();
2851
- await graph.zoomTo(z * 0.9);
2894
+ after_camera(gobj, graph.zoomTo(z * 0.9));
2852
2895
  }
2853
2896
 
2854
2897
  async function graph_render(gobj)
@@ -7502,7 +7545,79 @@ function ac_zoom_reset(gobj, event, kw, src)
7502
7545
  let priv = gobj.priv;
7503
7546
  let graph = priv.graph;
7504
7547
 
7505
- graph.zoomTo(1);
7548
+ /*
7549
+ * With an anchor, actual size means "this node, life size";
7550
+ * without one it leaves the camera where it was, as before.
7551
+ */
7552
+ if(priv.anchor_state === "on" && priv.anchor_id) {
7553
+ after_camera(gobj, graph.zoomTo(1));
7554
+ } else {
7555
+ graph.zoomTo(1);
7556
+ }
7557
+ return 0;
7558
+ }
7559
+
7560
+ /************************************************************
7561
+ * What the anchor button says it will do next.
7562
+ ************************************************************/
7563
+ function anchor_title(state)
7564
+ {
7565
+ if(state === "arming") {
7566
+ return t('click the element to centre on');
7567
+ }
7568
+ if(state === "on") {
7569
+ return t('centred: click to release');
7570
+ }
7571
+ return t('anchor view');
7572
+ }
7573
+
7574
+ /************************************************************
7575
+ * Every camera action ends the same way once an anchor is
7576
+ * set: the thing you were reading goes back to the middle.
7577
+ *
7578
+ * Chained on the promise G6 returns rather than run beside it
7579
+ * -- centring before the zoom has finished centres the OLD
7580
+ * view, and the reader watches the node slide away and come
7581
+ * back.
7582
+ ************************************************************/
7583
+ function after_camera(gobj, moved)
7584
+ {
7585
+ let priv = gobj.priv;
7586
+
7587
+ if(priv.anchor_state !== "on" || !priv.anchor_id) {
7588
+ return;
7589
+ }
7590
+ Promise.resolve(moved).then(function() {
7591
+ yui_graph_center_on(priv.graph, priv.anchor_id);
7592
+ }).catch(function() {
7593
+ /* a camera move that never finished has nothing to centre */
7594
+ });
7595
+ }
7596
+
7597
+ /************************************************************
7598
+ * Arm the anchor, or let it go.
7599
+ *
7600
+ * Three states and one button, so a press ADVANCES: with no
7601
+ * target it starts waiting for one, and with a target it
7602
+ * releases. Pressing it while it waits is a cancel, which is
7603
+ * what somebody who pressed it by mistake will try.
7604
+ *
7605
+ * Unlike the DOM toolbars, the button is not repainted in
7606
+ * place: this toolbar is a G6 plugin that rebuilds its whole
7607
+ * item list, and the pressed classes are baked into that list.
7608
+ ************************************************************/
7609
+ function ac_toggle_anchor(gobj, event, kw, src)
7610
+ {
7611
+ let priv = gobj.priv;
7612
+
7613
+ if(priv.anchor_state === "off") {
7614
+ priv.anchor_state = "arming";
7615
+ } else {
7616
+ priv.anchor_state = "off";
7617
+ priv.anchor_id = "";
7618
+ }
7619
+
7620
+ update_toolbar(gobj);
7506
7621
  return 0;
7507
7622
  }
7508
7623
 
@@ -7750,6 +7865,21 @@ function ac_node_click(gobj, event, kw, src)
7750
7865
  let graph = priv.graph;
7751
7866
  let node_id = kw.evt.target.id;
7752
7867
 
7868
+ /*
7869
+ * A waiting anchor TAKES the click, before selection, before
7870
+ * ports, before linking: it is what the reader armed the button
7871
+ * for, and letting the same press also do the graph's own thing
7872
+ * would make picking a target a side effect of doing something
7873
+ * else.
7874
+ */
7875
+ if(priv.anchor_state === "arming") {
7876
+ priv.anchor_id = node_id;
7877
+ priv.anchor_state = "on";
7878
+ update_toolbar(gobj);
7879
+ yui_graph_center_on(graph, node_id);
7880
+ return 0;
7881
+ }
7882
+
7753
7883
  try {
7754
7884
  let nodedata = graph.getNodeData(node_id);
7755
7885
  if(nodedata && nodedata.data && nodedata.data.desc) {
@@ -8061,6 +8191,7 @@ function create_gclass(gclass_name)
8061
8191
  ["EV_ZOOM_IN", ac_zoom_in, null],
8062
8192
  ["EV_ZOOM_OUT", ac_zoom_out, null],
8063
8193
  ["EV_ZOOM_RESET", ac_zoom_reset, null],
8194
+ ["EV_TOGGLE_ANCHOR", ac_toggle_anchor, null],
8064
8195
  ["EV_TOGGLE_TOOLBARS", ac_toggle_toolbars, null],
8065
8196
  ["EV_AUTO_FIT", ac_auto_fit, null],
8066
8197
  ["EV_ZOOM_SELECTION", ac_zoom_selection, null],
@@ -8110,6 +8241,7 @@ function create_gclass(gclass_name)
8110
8241
  ["EV_ZOOM_IN", 0],
8111
8242
  ["EV_ZOOM_OUT", 0],
8112
8243
  ["EV_ZOOM_RESET", 0],
8244
+ ["EV_TOGGLE_ANCHOR", 0],
8113
8245
  ["EV_TOGGLE_TOOLBARS", 0],
8114
8246
  ["EV_AUTO_FIT", 0],
8115
8247
  ["EV_ZOOM_SELECTION", 0],
@@ -42,6 +42,9 @@ import {
42
42
  import {yui_toolbar} from "./yui_toolbar.js";
43
43
  import {
44
44
  yui_graph_camera_items,
45
+ yui_graph_anchor_item,
46
+ yui_graph_update_anchor,
47
+ yui_graph_center_on,
45
48
  yui_graph_fold_items,
46
49
  yui_graph_refresh_item,
47
50
  yui_graph_update_zoom,
@@ -100,6 +103,19 @@ let PRIVATE_DATA = {
100
103
  collapse_state: null, // map of full_name -> "collapsed" | "expanded"
101
104
  resize_observer: null, // ResizeObserver on the canvas mount element
102
105
  _resize_raf: 0, // rAF id debouncing resize -> EV_RESIZE
106
+
107
+ /*---------------- camera anchor ----------------*/
108
+ /*
109
+ * NOT `pending_anchor`, which is a different job with the same
110
+ * word: that one keeps a node at the SAME SCREEN SPOT across a
111
+ * re-render, so expanding a branch does not throw the reader off
112
+ * the node they expanded. This one keeps a node in the MIDDLE
113
+ * across a ZOOM, and the reader chooses it from the toolbar.
114
+ */
115
+ anchor_id: "", // the node the camera keeps in the middle
116
+ anchor_name: "", // its full_name, which survives a rebuild
117
+ anchor_state: "off", // "off" | "arming" | "on"
118
+ last_zoom: 1, // tells a wheel zoom from a drag in aftertransform
103
119
  };
104
120
 
105
121
  let __gclass__ = null;
@@ -557,6 +573,7 @@ function make_toolbar(gobj)
557
573
  * drawings have to be the same drawings.
558
574
  */
559
575
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
576
+ center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
560
577
  center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
561
578
 
562
579
  /*
@@ -644,6 +661,27 @@ function build_graph(gobj)
644
661
  * wheel notch passes through no action of ours. */
645
662
  graph.on('aftertransform', () => {
646
663
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
664
+
665
+ /*
666
+ * A ZOOM re-centres on the anchor; a PAN does not.
667
+ * `aftertransform` fires for both, and an anchor that also
668
+ * answered a drag would make the graph impossible to move
669
+ * while it is set. The zoom LEVEL is what tells them apart:
670
+ * it changes on a wheel notch and never on a drag.
671
+ */
672
+ let z = priv.last_zoom;
673
+ try {
674
+ z = priv.graph.getZoom();
675
+ } catch(e) {
676
+ return;
677
+ }
678
+ if(z === priv.last_zoom) {
679
+ return;
680
+ }
681
+ priv.last_zoom = z;
682
+ if(priv.anchor_state === "on" && priv.anchor_id) {
683
+ yui_graph_center_on(priv.graph, priv.anchor_id);
684
+ }
647
685
  });
648
686
 
649
687
  graph.on(NodeEvent.CLICK, (evt) => {
@@ -1071,13 +1109,40 @@ function load_tree(gobj)
1071
1109
  build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
1072
1110
 
1073
1111
  if(nodes.length > 0) {
1112
+ let root_id = nodes[0].id; /* built before its children */
1074
1113
  graph.setData({nodes: nodes, edges: edges});
1075
1114
  graph.render().then(() => {
1115
+ /* Ids are generated per build: point the camera anchor at
1116
+ * its gobj again before anybody asks the camera to go
1117
+ * there. (`anchor` below is the OTHER one -- see the
1118
+ * note in PRIVATE_DATA.) */
1119
+ reanchor(gobj);
1120
+
1076
1121
  if(anchor) {
1077
1122
  restore_view_anchored(gobj, anchor);
1078
- } else if(!preserve_view) {
1079
- graph.fitView();
1123
+ return;
1080
1124
  }
1125
+ if(preserve_view) {
1126
+ return;
1127
+ }
1128
+
1129
+ /*
1130
+ * ACTUAL SIZE, not fit. A tree of anything real does not
1131
+ * fit at a zoom anybody can read, so fitting answered
1132
+ * "where is everything" when the question on opening is
1133
+ * "what does this say".
1134
+ *
1135
+ * Centred on the ANCHOR if there is one, else on the ROOT
1136
+ * -- never parked at the layout's origin, which is a
1137
+ * corner with nothing in it.
1138
+ */
1139
+ Promise.resolve(graph.zoomTo(1)).then(() => {
1140
+ if(!yui_graph_center_on(graph, priv.anchor_id)) {
1141
+ yui_graph_center_on(graph, root_id);
1142
+ }
1143
+ priv.last_zoom = 1;
1144
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
1145
+ });
1081
1146
  });
1082
1147
  }
1083
1148
  }
@@ -1861,6 +1926,91 @@ function ac_collapse_all(gobj, event, kw, src)
1861
1926
  return 0;
1862
1927
  }
1863
1928
 
1929
+ /************************************************************
1930
+ * Every camera action ends the same way once an anchor is
1931
+ * set: the thing you were reading goes back to the middle.
1932
+ *
1933
+ * Chained on the promise G6 returns rather than run beside it
1934
+ * -- centring before the zoom has finished centres the OLD
1935
+ * view, and the reader watches the node slide away and come
1936
+ * back.
1937
+ ************************************************************/
1938
+ function after_camera(gobj, moved)
1939
+ {
1940
+ let priv = gobj.priv;
1941
+
1942
+ if(priv.anchor_state !== "on" || !priv.anchor_id) {
1943
+ return;
1944
+ }
1945
+ Promise.resolve(moved).then(function() {
1946
+ yui_graph_center_on(priv.graph, priv.anchor_id);
1947
+ }).catch(function() {
1948
+ /* a camera move that never finished has nothing to centre */
1949
+ });
1950
+ }
1951
+
1952
+ /************************************************************
1953
+ * Point the anchor back at its node after a rebuild.
1954
+ *
1955
+ * Node ids are generated per build, so the id the reader chose
1956
+ * does not survive an expand, a collapse or a refresh -- the
1957
+ * `full_name` does, and it is what the anchor really means:
1958
+ * "this gobj", not "this card".
1959
+ *
1960
+ * A gobj no longer drawn (its branch got collapsed, or it went
1961
+ * away) releases the anchor rather than leaving a button that
1962
+ * claims to be locked onto something invisible.
1963
+ ************************************************************/
1964
+ function reanchor(gobj)
1965
+ {
1966
+ let priv = gobj.priv;
1967
+
1968
+ if(priv.anchor_state !== "on" || !priv.anchor_name) {
1969
+ return;
1970
+ }
1971
+
1972
+ let found = "";
1973
+ if(priv.node_by_id) {
1974
+ for(let id of Object.keys(priv.node_by_id)) {
1975
+ if(priv.node_by_id[id].full_name === priv.anchor_name) {
1976
+ found = id;
1977
+ break;
1978
+ }
1979
+ }
1980
+ }
1981
+
1982
+ priv.anchor_id = found;
1983
+ if(!found) {
1984
+ priv.anchor_state = "off";
1985
+ priv.anchor_name = "";
1986
+ }
1987
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
1988
+ }
1989
+
1990
+ /************************************************************
1991
+ * Arm the anchor, or let it go.
1992
+ *
1993
+ * Three states and one button, so a press ADVANCES: with no
1994
+ * target it starts waiting for one, and with a target it
1995
+ * releases. Pressing it while it waits is a cancel, which is
1996
+ * what somebody who pressed it by mistake will try.
1997
+ ************************************************************/
1998
+ function ac_toggle_anchor(gobj, event, kw, src)
1999
+ {
2000
+ let priv = gobj.priv;
2001
+
2002
+ if(priv.anchor_state === "off") {
2003
+ priv.anchor_state = "arming";
2004
+ } else {
2005
+ priv.anchor_state = "off";
2006
+ priv.anchor_id = "";
2007
+ priv.anchor_name = "";
2008
+ }
2009
+
2010
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
2011
+ return 0;
2012
+ }
2013
+
1864
2014
  /************************************************************
1865
2015
  *
1866
2016
  ************************************************************/
@@ -1869,7 +2019,7 @@ function ac_zoom_in(gobj, event, kw, src)
1869
2019
  let graph = gobj.priv.graph;
1870
2020
  if(graph) {
1871
2021
  let z = graph.getZoom();
1872
- graph.zoomTo(z * 1.2);
2022
+ after_camera(gobj, graph.zoomTo(z * 1.2));
1873
2023
  }
1874
2024
  return 0;
1875
2025
  }
@@ -1882,7 +2032,7 @@ function ac_zoom_out(gobj, event, kw, src)
1882
2032
  let graph = gobj.priv.graph;
1883
2033
  if(graph) {
1884
2034
  let z = graph.getZoom();
1885
- graph.zoomTo(z * 0.8);
2035
+ after_camera(gobj, graph.zoomTo(z * 0.8));
1886
2036
  }
1887
2037
  return 0;
1888
2038
  }
@@ -1892,10 +2042,21 @@ function ac_zoom_out(gobj, event, kw, src)
1892
2042
  ************************************************************/
1893
2043
  function ac_zoom_reset(gobj, event, kw, src)
1894
2044
  {
1895
- let graph = gobj.priv.graph;
2045
+ let priv = gobj.priv;
2046
+ let graph = priv.graph;
2047
+
1896
2048
  if(graph && graph.rendered) {
1897
- graph.zoomTo(1);
1898
- graph.translateTo([0, 0]);
2049
+ /*
2050
+ * With an anchor, actual size means "this node, life size".
2051
+ * Without one it means the layout's origin, which is where
2052
+ * `1:1` always landed -- a corner nobody was looking at.
2053
+ */
2054
+ if(priv.anchor_state === "on" && priv.anchor_id) {
2055
+ after_camera(gobj, graph.zoomTo(1));
2056
+ } else {
2057
+ graph.zoomTo(1);
2058
+ graph.translateTo([0, 0]);
2059
+ }
1899
2060
  }
1900
2061
  return 0;
1901
2062
  }
@@ -1940,6 +2101,21 @@ function ac_node_click(gobj, event, kw, src)
1940
2101
  return 0;
1941
2102
  }
1942
2103
 
2104
+ /*
2105
+ * A waiting anchor TAKES the click: it is what the reader armed
2106
+ * the button for, and letting the same press also open the
2107
+ * popover would make picking a target a side effect of doing
2108
+ * something else.
2109
+ */
2110
+ if(priv.anchor_state === "arming") {
2111
+ priv.anchor_id = node_id;
2112
+ priv.anchor_name = node_data.full_name || "";
2113
+ priv.anchor_state = "on";
2114
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
2115
+ yui_graph_center_on(graph, node_id);
2116
+ return 0;
2117
+ }
2118
+
1943
2119
  /*
1944
2120
  * Show popover at click position
1945
2121
  */
@@ -2065,6 +2241,7 @@ function create_gclass(gclass_name)
2065
2241
  ["EV_ZOOM_RESET", ac_zoom_reset, null],
2066
2242
  ["EV_CENTER", ac_center, null],
2067
2243
  ["EV_NODE_CLICK", ac_node_click, null],
2244
+ ["EV_TOGGLE_ANCHOR", ac_toggle_anchor, null],
2068
2245
  ["EV_RESIZE", ac_resize, null],
2069
2246
  ["EV_SHOW", ac_show, null],
2070
2247
  ["EV_HIDE", ac_hide, null],
@@ -2086,6 +2263,7 @@ function create_gclass(gclass_name)
2086
2263
  ["EV_ZOOM_RESET", 0],
2087
2264
  ["EV_CENTER", 0],
2088
2265
  ["EV_NODE_CLICK", 0],
2266
+ ["EV_TOGGLE_ANCHOR", 0],
2089
2267
  ["EV_RESIZE", 0],
2090
2268
  ["EV_SHOW", 0],
2091
2269
  ["EV_HIDE", 0],