@yuneta/gobj-ui 7.23.27 → 7.23.29

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.27",
3
+ "version": "7.23.29",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -1273,8 +1273,11 @@ function configure_toolbar(gobj)
1273
1273
  * the rebuild came back OFF. */
1274
1274
  {
1275
1275
  id: 'g6-icon-anchor', value: 'anchor',
1276
+ /* PRESSED is "on" and only "on": pressed is the
1277
+ * state the control is IN, and while it is
1278
+ * arming it is asking for something instead. */
1276
1279
  className: 'EV_TOGGLE_ANCHOR' +
1277
- (priv.anchor_state !== "off"? ' pressed_state' : '') +
1280
+ (priv.anchor_state === "on"? ' pressed_state' : '') +
1278
1281
  (priv.anchor_state === "arming"? ' color_pending_state' : ''),
1279
1282
  title: anchor_title(priv.anchor_state)
1280
1283
  },
@@ -1600,6 +1603,37 @@ function configure_behaviour(gobj)
1600
1603
  graph_write_behaviors(gobj, behaviors);
1601
1604
  }
1602
1605
 
1606
+ /************************************************************
1607
+ * Count hooks and fkeys in topic desc, classify node type
1608
+ ************************************************************/
1609
+ function calculate_hooks_fkeys_counter(desc)
1610
+ {
1611
+ let cols = desc.cols;
1612
+ desc.hooks_counter = 0;
1613
+ desc.fkeys_counter = 0;
1614
+
1615
+ for(let i=0; i<cols.length; i++) {
1616
+ let col = cols[i];
1617
+ const field_desc = treedb_get_field_desc(col);
1618
+ switch(field_desc.type) {
1619
+ case "hook":
1620
+ desc.hooks_counter++;
1621
+ break;
1622
+ case "fkey":
1623
+ desc.fkeys_counter++;
1624
+ break;
1625
+ }
1626
+ }
1627
+
1628
+ if(desc.hooks_counter === 0) {
1629
+ desc.node_treedb_type = 'child';
1630
+ } else if(desc.fkeys_counter === 0) {
1631
+ desc.node_treedb_type = 'extended';
1632
+ } else {
1633
+ desc.node_treedb_type = 'hierarchical';
1634
+ }
1635
+ }
1636
+
1603
1637
  /************************************************************
1604
1638
  * Build ports for a topic desc
1605
1639
  ************************************************************/
@@ -5712,7 +5746,7 @@ function refresh_minimap(gobj)
5712
5746
  * same three-way choice — it lived inline in the theme refresh and is
5713
5747
  * shared now. Returns null for a node that carries no desc.
5714
5748
  ************************************************************/
5715
- function node_innerHTML_of(nd, theme, highlight, selected)
5749
+ function node_innerHTML_of(nd, theme, highlight, selected, anchored)
5716
5750
  {
5717
5751
  if(!nd || !nd.data || !nd.data.desc) {
5718
5752
  return null;
@@ -5725,17 +5759,17 @@ function node_innerHTML_of(nd, theme, highlight, selected)
5725
5759
  case 'child':
5726
5760
  return build_chip_innerHTML(
5727
5761
  desc.color, theme, record.icon, label, record.id,
5728
- highlight, selected
5762
+ highlight, selected, anchored
5729
5763
  );
5730
5764
  case 'extended':
5731
5765
  return build_node_innerHTML(
5732
5766
  desc.color, theme, record.icon, label,
5733
- desc.topic_name, true, record.id, highlight, selected
5767
+ desc.topic_name, true, record.id, highlight, selected, anchored
5734
5768
  );
5735
5769
  case 'hierarchical':
5736
5770
  return build_node_innerHTML(
5737
5771
  desc.color, theme, record.icon, label,
5738
- desc.topic_name, false, record.id, highlight, selected
5772
+ desc.topic_name, false, record.id, highlight, selected, anchored
5739
5773
  );
5740
5774
  }
5741
5775
  return null;
@@ -5765,7 +5799,8 @@ function repaint_cards(gobj, ids)
5765
5799
  let updates = [];
5766
5800
  for(let id of ids) {
5767
5801
  let html = node_innerHTML_of(
5768
- graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id)
5802
+ graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id),
5803
+ priv.anchor_state === "on" && id === priv.anchor_id
5769
5804
  );
5770
5805
  if(html !== null) {
5771
5806
  updates.push({id: id, style: {innerHTML: html}});
@@ -5871,6 +5906,25 @@ function refresh_default_edges_theme(gobj, theme)
5871
5906
  * it, which is also the order that reads correctly when only one
5872
5907
  * of them is on.
5873
5908
  ************************************************************/
5909
+ /************************************************************
5910
+ * The mark of the card the CAMERA is holding.
5911
+ *
5912
+ * An outline and not another ring, and DASHED: the two rings
5913
+ * already mean things here -- amber is a find match or a
5914
+ * focused topic, blue is the selection -- and a third solid
5915
+ * ring would be a third thing to learn in the same channel.
5916
+ * A dash is a different channel, so it reads beside either of
5917
+ * them, and it says "provisional" the way a camera state is.
5918
+ *
5919
+ * The same dash the other two viewers draw: this is one
5920
+ * feature with one control, and it should not be three
5921
+ * drawings.
5922
+ ************************************************************/
5923
+ function anchor_outline(anchored)
5924
+ {
5925
+ return anchored? "outline: 3px dashed #fa8c16; outline-offset: 2px;" : "";
5926
+ }
5927
+
5874
5928
  function ring_shadow(highlight, selected, base_shadow)
5875
5929
  {
5876
5930
  let rings = [];
@@ -5897,7 +5951,7 @@ function ring_shadow(highlight, selected, base_shadow)
5897
5951
  * but lighter (1px border, no shadow, single line). The name is
5898
5952
  * always legible (ellipsis + native title tooltip on overflow).
5899
5953
  ************************************************************/
5900
- function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected)
5954
+ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected, anchored)
5901
5955
  {
5902
5956
  let title = key || label;
5903
5957
  let dark = (theme === "dark");
@@ -5922,13 +5976,14 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
5922
5976
  }
5923
5977
 
5924
5978
  return `
5925
- <div title="${escapeHtml(title)}" style="
5979
+ <div class="TREEDB_CARD" title="${escapeHtml(title)}" style="
5926
5980
  box-sizing: border-box;
5927
5981
  width: 100%;
5928
5982
  height: 100%;
5929
5983
  background: ${bg};
5930
5984
  border: ${highlight? "3px" : "1px"} solid ${border};
5931
5985
  ${ring_shadow(highlight, selected, "")}
5986
+ ${anchor_outline(anchored)}
5932
5987
  border-radius: 8px;
5933
5988
  color: ${text_color};
5934
5989
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -5955,7 +6010,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
5955
6010
  * colour is kept (per-topic differentiation) but softened via
5956
6011
  * color-mix instead of a harsh saturated fill. Theme-aware.
5957
6012
  ************************************************************/
5958
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected)
6013
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored)
5959
6014
  {
5960
6015
  let title = key || label;
5961
6016
  let dark = (theme === "dark");
@@ -6011,7 +6066,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
6011
6066
  }
6012
6067
 
6013
6068
  return `
6014
- <div title="${escapeHtml(title)}" style="
6069
+ <div class="TREEDB_CARD" title="${escapeHtml(title)}" style="
6015
6070
  box-sizing: border-box;
6016
6071
  width: 100%;
6017
6072
  height: 100%;
@@ -6019,6 +6074,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
6019
6074
  border: ${highlight? "3px" : "1.5px"} ${border_style} ${border};
6020
6075
  border-radius: 10px;
6021
6076
  ${ring_shadow(highlight, selected, shadow)}
6077
+ ${anchor_outline(anchored)}
6022
6078
  color: ${title_color};
6023
6079
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
6024
6080
  display: flex;
@@ -7571,6 +7627,7 @@ function after_camera(gobj, moved)
7571
7627
  function ac_toggle_anchor(gobj, event, kw, src)
7572
7628
  {
7573
7629
  let priv = gobj.priv;
7630
+ let was = priv.anchor_id;
7574
7631
 
7575
7632
  if(priv.anchor_state === "off") {
7576
7633
  priv.anchor_state = "arming";
@@ -7580,6 +7637,9 @@ function ac_toggle_anchor(gobj, event, kw, src)
7580
7637
  }
7581
7638
 
7582
7639
  update_toolbar(gobj);
7640
+ if(was) {
7641
+ repaint_cards(gobj, new Set([was]));
7642
+ }
7583
7643
  return 0;
7584
7644
  }
7585
7645
 
@@ -7838,6 +7898,7 @@ function ac_node_click(gobj, event, kw, src)
7838
7898
  priv.anchor_id = node_id;
7839
7899
  priv.anchor_state = "on";
7840
7900
  update_toolbar(gobj);
7901
+ repaint_cards(gobj, new Set([node_id]));
7841
7902
 
7842
7903
  /* POSTED, not made here: a camera move issued inside G6's click
7843
7904
  * dispatch is swallowed -- see the note in yui_graph_center_on.
@@ -22,6 +22,8 @@ import {
22
22
  gobj_write_attr,
23
23
  gobj_send_event,
24
24
  gobj_post_event,
25
+ kw_get_local_storage_value,
26
+ kw_set_local_storage_value,
25
27
  gobj_write_str_attr,
26
28
  gobj_read_str_attr,
27
29
  gobj_publish_event,
@@ -120,6 +122,123 @@ const ZOOM_CANVAS = {type: 'zoom-canvas', animation: false};
120
122
  const BEHAVIORS_READING = ['drag-canvas', ZOOM_CANVAS, 'drag-element'];
121
123
  const BEHAVIORS_PICKING = ['drag-canvas', ZOOM_CANVAS];
122
124
 
125
+ /*
126
+ * Where this view is remembered between visits.
127
+ *
128
+ * In localStorage and not in a persistent attr, for the reason the
129
+ * framework gives itself: only a SERVICE can load or save persistent
130
+ * attrs, and this gclass is hosted as a child. It is also the right
131
+ * place on its own terms -- how somebody left THEIR tree arranged is
132
+ * a fact about that browser, not about the yuno.
133
+ *
134
+ * Keyed by the gobj's name so two trees in one app do not overwrite
135
+ * each other's arrangement.
136
+ */
137
+ const VIEW_STORE_PREFIX = "yui_gobj_tree_view:";
138
+
139
+ /* What is worth carrying back: the layout, where the camera was, and
140
+ * which branches were folded. The anchor goes too -- it is a camera
141
+ * state like the other two, and coming back to a tree still holding
142
+ * the node you left it on is the point of holding it. */
143
+ function view_store_key(gobj)
144
+ {
145
+ return VIEW_STORE_PREFIX + gobj_name(gobj);
146
+ }
147
+
148
+ function restore_view_shape(gobj)
149
+ {
150
+ let priv = gobj.priv;
151
+ let state = load_view_state(gobj);
152
+
153
+ if(!state) {
154
+ return;
155
+ }
156
+ if(state.layout && LAYOUTS[state.layout]) {
157
+ gobj_write_str_attr(gobj, "layout", state.layout);
158
+ /* And the picker with it: the toolbar was built from the
159
+ * attr's default a moment ago, so without this the graph draws
160
+ * the remembered layout while the control names another. */
161
+ if(priv.$layout_select) {
162
+ priv.$layout_select.value = state.layout;
163
+ }
164
+ }
165
+ if(state.collapsed && typeof state.collapsed === "object") {
166
+ priv.collapse_state = Object.assign({}, state.collapsed);
167
+ }
168
+ }
169
+
170
+ function restore_view_state(gobj)
171
+ {
172
+ let priv = gobj.priv;
173
+ let graph = priv.graph;
174
+ let state = load_view_state(gobj);
175
+
176
+ if(!graph || !state) {
177
+ return false;
178
+ }
179
+
180
+ let done = false;
181
+ priv.restoring = true;
182
+ try {
183
+ if(typeof state.zoom === "number" && state.zoom > 0) {
184
+ graph.zoomTo(state.zoom);
185
+ priv.last_zoom = state.zoom;
186
+ done = true;
187
+ }
188
+ if(Array.isArray(state.position) && state.position.length >= 2) {
189
+ graph.translateTo([state.position[0], state.position[1]]);
190
+ done = true;
191
+ }
192
+ if(state.anchor) {
193
+ /* By NAME, never by node id: ids are generated per build,
194
+ * and this one was written in another session. */
195
+ priv.anchor_name = state.anchor;
196
+ priv.anchor_state = "on";
197
+ reanchor(gobj);
198
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
199
+ paint_anchor_mark(gobj);
200
+ }
201
+ } catch(e) {
202
+ log_error(`${gobj_short_name(gobj)}: cannot restore the saved view: ${e}`);
203
+ } finally {
204
+ priv.restoring = false;
205
+ }
206
+
207
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
208
+ return done;
209
+ }
210
+
211
+ function save_view_state(gobj)
212
+ {
213
+ let priv = gobj.priv;
214
+ let graph = priv.graph;
215
+
216
+ if(!graph || priv.restoring) {
217
+ return; /* mid-restore: that is our own writing coming back */
218
+ }
219
+
220
+ let state = {
221
+ layout: gobj_read_str_attr(gobj, "layout"),
222
+ collapsed: priv.collapse_state || {},
223
+ anchor: (priv.anchor_state === "on")? priv.anchor_name: "",
224
+ };
225
+ try {
226
+ state.zoom = graph.getZoom();
227
+ state.position = graph.getPosition();
228
+ } catch(e) {
229
+ /* between renders: keep the rest, the camera is not readable */
230
+ }
231
+
232
+ kw_set_local_storage_value(view_store_key(gobj), state);
233
+ }
234
+
235
+ function load_view_state(gobj)
236
+ {
237
+ let state = kw_get_local_storage_value(view_store_key(gobj), null, false);
238
+
239
+ return (state && typeof state === "object")? state: null;
240
+ }
241
+
123
242
  let PRIVATE_DATA = {
124
243
  canvas_id: "",
125
244
  graph: null,
@@ -146,6 +265,8 @@ let PRIVATE_DATA = {
146
265
  anchor_id: "", // the node the camera keeps in the middle
147
266
  anchor_name: "", // its full_name, which survives a rebuild
148
267
  anchor_state: "off", // "off" | "arming" | "on"
268
+ restoring: false, // guards the store against its own restore
269
+ restored: false, // the saved arrangement is applied ONCE
149
270
  last_zoom: 1, // tells a wheel zoom from a drag in aftertransform
150
271
  };
151
272
 
@@ -369,6 +490,14 @@ function mt_create(gobj)
369
490
  ***************************************************************/
370
491
  function mt_start(gobj)
371
492
  {
493
+ /*
494
+ * The layout and the folds go back BEFORE the first build: they
495
+ * decide what is built, so restoring them afterwards would mean
496
+ * building the tree twice and showing the wrong one first.
497
+ * The camera goes back after, once there is something to point at.
498
+ */
499
+ restore_view_shape(gobj);
500
+
372
501
  build_graph(gobj);
373
502
  load_tree(gobj);
374
503
  }
@@ -530,8 +659,15 @@ function build_popover(gobj, $container)
530
659
 
531
660
  let $close = document.createElement("button");
532
661
  $close.type = "button";
662
+ /* Named so a stylesheet can reach it: the size a FINGER needs is a
663
+ * media query, and a media query cannot beat an inline style
664
+ * without one to hang off. See `lib_graph.css`. */
665
+ $close.className = "GOBJ_TREE_POPOVER_CLOSE";
533
666
  $close.textContent = "×";
534
667
  $close.title = t("close");
668
+ $close.setAttribute("data-i18n-title", "close");
669
+ $close.setAttribute("aria-label", t("close"));
670
+ $close.setAttribute("data-i18n-aria-label", "close");
535
671
  $close.style.cssText = `
536
672
  background: transparent;
537
673
  border: none;
@@ -710,6 +846,10 @@ function build_graph(gobj)
710
846
  if(priv.anchor_state === "on" && priv.anchor_id) {
711
847
  yui_graph_center_on(priv.graph, priv.anchor_id);
712
848
  }
849
+ /* Where the reader left the camera. One hook covers the wheel,
850
+ * the drag and every zoom button, which is why it is not
851
+ * written into each of them. */
852
+ save_view_state(gobj);
713
853
  });
714
854
 
715
855
  graph.on(NodeEvent.CLICK, (evt) => {
@@ -947,7 +1087,7 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
947
1087
  );
948
1088
 
949
1089
  node_html = `
950
- <div style="
1090
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
951
1091
  width: ${width}px;
952
1092
  height: ${height}px;
953
1093
  background: ${cs.bg};
@@ -999,7 +1139,7 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
999
1139
  ).join("");
1000
1140
 
1001
1141
  node_html = `
1002
- <div style="
1142
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
1003
1143
  width: ${width}px;
1004
1144
  height: ${height}px;
1005
1145
  background: ${cs.bg};
@@ -1145,6 +1285,7 @@ function load_tree(gobj)
1145
1285
  * there. (`anchor` below is the OTHER one -- see the
1146
1286
  * note in PRIVATE_DATA.) */
1147
1287
  reanchor(gobj);
1288
+ paint_anchor_mark(gobj);
1148
1289
 
1149
1290
  if(anchor) {
1150
1291
  restore_view_anchored(gobj, anchor);
@@ -1154,6 +1295,19 @@ function load_tree(gobj)
1154
1295
  return;
1155
1296
  }
1156
1297
 
1298
+ /*
1299
+ * The arrangement this browser was left in, once: after
1300
+ * that the reader's own camera wins, and a restore on
1301
+ * every rebuild would drag them back every time they
1302
+ * folded something.
1303
+ */
1304
+ if(!priv.restored) {
1305
+ priv.restored = true;
1306
+ if(restore_view_state(gobj)) {
1307
+ return;
1308
+ }
1309
+ }
1310
+
1157
1311
  /*
1158
1312
  * ACTUAL SIZE, not fit. A tree of anything real does not
1159
1313
  * fit at a zoom anybody can read, so fitting answered
@@ -1795,6 +1949,7 @@ class GobjLanesHLayout extends BaseLayout {
1795
1949
  ************************************************************/
1796
1950
  function ac_refresh(gobj, event, kw, src)
1797
1951
  {
1952
+ save_view_state(gobj);
1798
1953
  refresh_tree(gobj);
1799
1954
  return 0;
1800
1955
  }
@@ -1841,10 +1996,12 @@ function ac_change_layout(gobj, event, kw, src)
1841
1996
  }
1842
1997
 
1843
1998
  apply_layout(gobj);
1999
+ save_view_state(gobj);
1844
2000
 
1845
2001
  /*
1846
2002
  * Rebuild nodes because compact/full HTML differs per layout
1847
2003
  */
2004
+ save_view_state(gobj);
1848
2005
  refresh_tree(gobj);
1849
2006
 
1850
2007
  return 0;
@@ -1902,6 +2059,7 @@ function ac_toggle_collapse(gobj, event, kw, src)
1902
2059
  priv.collapse_state[node_data.full_name] = "collapsed";
1903
2060
  }
1904
2061
 
2062
+ save_view_state(gobj);
1905
2063
  refresh_tree(gobj, {anchor: anchor});
1906
2064
  return 0;
1907
2065
  }
@@ -2022,12 +2180,46 @@ function ac_center_anchor(gobj, event, kw, src)
2022
2180
  {
2023
2181
  let priv = gobj.priv;
2024
2182
 
2183
+ paint_anchor_mark(gobj);
2184
+ save_view_state(gobj);
2025
2185
  if(priv.anchor_state === "on" && priv.anchor_id) {
2026
2186
  yui_graph_center_on(priv.graph, priv.anchor_id);
2027
2187
  }
2028
2188
  return 0;
2029
2189
  }
2030
2190
 
2191
+ /************************************************************
2192
+ * Mark the card the camera is holding.
2193
+ *
2194
+ * A class on the card's own element and not a G6 node state:
2195
+ * G6 paints no state style on an html node.
2196
+ *
2197
+ * Re-applied after every rebuild, because the cards are new
2198
+ * DOM each time and the mark would otherwise last until the
2199
+ * next expand.
2200
+ ************************************************************/
2201
+ function paint_anchor_mark(gobj)
2202
+ {
2203
+ let priv = gobj.priv;
2204
+ let $container = gobj_read_attr(gobj, "$container");
2205
+
2206
+ if(!$container) {
2207
+ return;
2208
+ }
2209
+ for(let $card of $container.querySelectorAll('.GOBJ_CARD_ANCHORED')) {
2210
+ $card.classList.remove('GOBJ_CARD_ANCHORED');
2211
+ }
2212
+ if(priv.anchor_state !== "on" || !priv.anchor_name) {
2213
+ return;
2214
+ }
2215
+ let $it = $container.querySelector(
2216
+ '.GOBJ_CARD[data-gobj-name="' + CSS.escape(priv.anchor_name) + '"]'
2217
+ );
2218
+ if($it) {
2219
+ $it.classList.add('GOBJ_CARD_ANCHORED');
2220
+ }
2221
+ }
2222
+
2031
2223
  /************************************************************
2032
2224
  * Reading, or picking the node to anchor.
2033
2225
  ************************************************************/
@@ -2068,6 +2260,8 @@ function ac_toggle_anchor(gobj, event, kw, src)
2068
2260
 
2069
2261
  yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
2070
2262
  set_picking_mode(gobj, priv.anchor_state === "arming");
2263
+ paint_anchor_mark(gobj);
2264
+ save_view_state(gobj);
2071
2265
  return 0;
2072
2266
  }
2073
2267
 
@@ -2202,6 +2396,7 @@ function ac_show(gobj, event, kw, src)
2202
2396
  /*
2203
2397
  * Always refresh on show: the tree may have changed while hidden
2204
2398
  */
2399
+ save_view_state(gobj);
2205
2400
  refresh_tree(gobj);
2206
2401
 
2207
2402
  if(graph) {