@yuneta/gobj-ui 7.23.28 → 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.
@@ -54,6 +54,8 @@ import {
54
54
  yui_graph_anchor_item,
55
55
  yui_graph_update_anchor,
56
56
  yui_graph_center_on,
57
+ yui_graph_place_at,
58
+ yui_graph_viewport_of,
57
59
  yui_graph_fold_items,
58
60
  yui_graph_refresh_item,
59
61
  yui_graph_update_zoom,
@@ -192,6 +194,7 @@ let PRIVATE_DATA = {
192
194
  /*---------------- find + collapse ----------------*/
193
195
  search: "", // current find term, lower-cased
194
196
  collapsed: null, // Set<string> of paths whose children are hidden
197
+ reserved: null, // Map<path,width> the space a fold leaves behind
195
198
  match_count: 0, // matches of the last find, for the count chip
196
199
  find_timer: null, // rate-limits the find box
197
200
  fold_listener: null, // delegated click on the card fold handles
@@ -206,6 +209,7 @@ let PRIVATE_DATA = {
206
209
  anchor_state: "off", // "off" | "arming" | "on"
207
210
  last_zoom: 1, // tells a wheel zoom from a drag in aftertransform
208
211
  center_posted: false, // one re-centring in flight, never a queue of them
212
+ pending_keep: null, // {path,x,y}: a card to put back where it was
209
213
  };
210
214
 
211
215
  let __gclass__ = null;
@@ -331,6 +335,7 @@ function mt_create(gobj)
331
335
  let priv = gobj.priv;
332
336
 
333
337
  priv.collapsed = new Set();
338
+ priv.reserved = new Map();
334
339
  priv.search = "";
335
340
 
336
341
  let name = clean_name(gobj_name(gobj));
@@ -475,8 +480,15 @@ function build_ui(gobj)
475
480
  if(evt.type !== "click") {
476
481
  return; /* swallowed so G6 never sees a node click */
477
482
  }
478
- gobj_send_event(gobj, "EV_TOGGLE_FOLD",
479
- {path: $handle.getAttribute('data-fold-path')}, gobj);
483
+ /* Which CARD the chip sits on, not only what it folds: a
484
+ * header chip folds its own card, a row chip folds a child
485
+ * that has none -- and the card that must not move is the
486
+ * one under the finger either way. */
487
+ let $card = $handle.closest? $handle.closest('.JSON_CARD'): null;
488
+ gobj_send_event(gobj, "EV_TOGGLE_FOLD", {
489
+ path: $handle.getAttribute('data-fold-path'),
490
+ keep: $card? $card.getAttribute('data-json-path'): ""
491
+ }, gobj);
480
492
  };
481
493
  for(let type of FOLD_SWALLOWED_EVENTS) {
482
494
  $mount.addEventListener(type, priv.fold_listener, true);
@@ -944,6 +956,13 @@ const BEHAVIORS_PICKING = ['drag-canvas', ZOOM_CANVAS];
944
956
  const CARD_HEADER_H = 24;
945
957
  const CARD_ROW_H = 22;
946
958
 
959
+ /* The tree layout's gaps. Module scope because the width a fold
960
+ * RESERVES has to be measured with the very same numbers the layout
961
+ * packs with, or the hole is the wrong size and everything shifts by
962
+ * the difference. */
963
+ const H_GAP = 60; // horizontal gap between siblings
964
+ const V_GAP = 50; // vertical gap between levels
965
+
947
966
  function fold_toggle_html(path, foldable, folded, branches, colors)
948
967
  {
949
968
  if(!foldable) {
@@ -1135,6 +1154,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
1135
1154
  */
1136
1155
  if(parent_id && is_pure_collection(kw)) {
1137
1156
  if(priv.collapsed.has(path)) {
1157
+ /* Folded, and it draws nothing of its own -- so the space
1158
+ * its children were taking is left HERE, in their place
1159
+ * among the parent's other children. */
1160
+ push_fold_phantom(gobj, path, parent_id, parent_port, nodes, edges);
1138
1161
  return parent_id;
1139
1162
  }
1140
1163
  for(let pending of pending_complex) {
@@ -1281,6 +1304,9 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
1281
1304
  data: {
1282
1305
  path: path,
1283
1306
  is_dict: is_dict,
1307
+ /* Only a folded card carries one; the layout leaves the
1308
+ * space its subtree used to take. */
1309
+
1284
1310
  },
1285
1311
  style: {
1286
1312
  innerHTML: node_html,
@@ -1334,6 +1360,9 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
1334
1360
  * Recurse into complex children — unless this card is folded.
1335
1361
  */
1336
1362
  if(folded) {
1363
+ /* The card stays; the space under it is held by a phantom of
1364
+ * its own, so its siblings do not close in over it. */
1365
+ push_fold_phantom(gobj, path, group_id, "", nodes, edges);
1337
1366
  return group_id;
1338
1367
  }
1339
1368
  for(let pending of pending_complex) {
@@ -1395,6 +1424,16 @@ function load_json(gobj)
1395
1424
  paint_anchor_mark(gobj);
1396
1425
 
1397
1426
  if(preserve_view) {
1427
+ let keep = priv.pending_keep;
1428
+ priv.pending_keep = null;
1429
+ if(keep) {
1430
+ for(let n of nodes) {
1431
+ if(n.data && n.data.path === keep.path) {
1432
+ yui_graph_place_at(graph, n.id, [keep.x, keep.y]);
1433
+ break;
1434
+ }
1435
+ }
1436
+ }
1398
1437
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
1399
1438
  return;
1400
1439
  }
@@ -1476,6 +1515,131 @@ function update_find_result(gobj)
1476
1515
  priv.$find_result.classList.remove("is-hidden");
1477
1516
  }
1478
1517
 
1518
+ /************************************************************
1519
+ * The invisible child a fold leaves behind.
1520
+ *
1521
+ * It is the whole of "nothing moves when I fold": the layout
1522
+ * is a plain tree that centres a parent over its children, so
1523
+ * the only way to keep the survivors still is to leave the
1524
+ * space where it was, in the order it was, as a CHILD. A width
1525
+ * reserved as a number would still have to be placed, and
1526
+ * appending it moves every sibling by half of it.
1527
+ *
1528
+ * A `rect` and not an `html` node: nothing is drawn, and a
1529
+ * DOM element per fold would be a card nobody can see but the
1530
+ * Inspector would show.
1531
+ ************************************************************/
1532
+ function push_fold_phantom(gobj, path, parent_id, parent_port, nodes, edges)
1533
+ {
1534
+ let priv = gobj.priv;
1535
+ let width = priv.reserved.get(path) || 0;
1536
+
1537
+ if(!(width > 0) || !parent_id) {
1538
+ return; /* never measured: nothing to reserve */
1539
+ }
1540
+
1541
+ let id = gen_node_id(gobj);
1542
+ nodes.push({
1543
+ id: id,
1544
+ type: 'rect',
1545
+ data: {path: path, phantom: true},
1546
+ style: {
1547
+ size: [width, 1],
1548
+ fill: 'transparent',
1549
+ stroke: 'transparent',
1550
+ opacity: 0,
1551
+ pointerEvents: 'none',
1552
+ },
1553
+ });
1554
+ edges.push({
1555
+ source: parent_id,
1556
+ target: id,
1557
+ style: {opacity: 0, endArrow: false, sourcePort: parent_port || undefined},
1558
+ });
1559
+ }
1560
+
1561
+ /************************************************************
1562
+ * The width the children a fold is about to hide are taking
1563
+ * RIGHT NOW, measured with the layout's own numbers.
1564
+ *
1565
+ * `card_path` is the card that will lose them; `under` limits
1566
+ * it to the ones hanging from one key of that card, which is
1567
+ * the case of a container that has no card of its own -- its
1568
+ * children hang from the parent, so its fold is a hole in the
1569
+ * PARENT.
1570
+ ************************************************************/
1571
+ function group_width_of(gobj, card_path, under)
1572
+ {
1573
+ let priv = gobj.priv;
1574
+
1575
+ if(!priv.graph) {
1576
+ return 0;
1577
+ }
1578
+ try {
1579
+ let by_id = {};
1580
+ let card_id = "";
1581
+ for(let node of priv.graph.getNodeData()) {
1582
+ if(!node || !node.data) {
1583
+ continue;
1584
+ }
1585
+ by_id[node.id] = node;
1586
+ if(node.data.path === card_path) {
1587
+ card_id = node.id;
1588
+ }
1589
+ }
1590
+ if(!card_id) {
1591
+ return 0;
1592
+ }
1593
+
1594
+ let prefix = under? (under + "`"): "";
1595
+ let sum = 0;
1596
+ let n = 0;
1597
+ for(let edge of priv.graph.getEdgeData()) {
1598
+ if(!edge || edge.source !== card_id) {
1599
+ continue;
1600
+ }
1601
+ let kid = by_id[edge.target];
1602
+ if(!kid) {
1603
+ continue;
1604
+ }
1605
+ if(prefix && String(kid.data.path).indexOf(prefix) !== 0) {
1606
+ continue;
1607
+ }
1608
+ sum += kid.data.subtree_width || 0;
1609
+ n++;
1610
+ }
1611
+ return n? (sum + H_GAP * (n - 1)): 0;
1612
+ } catch(e) {
1613
+ return 0; /* between renders: nothing measured */
1614
+ }
1615
+ }
1616
+
1617
+ /************************************************************
1618
+ * The screen spot of the card at `path`, to be given back
1619
+ * after the rebuild. Null when it has no card of its own --
1620
+ * a pure collection -- and then nothing anchors and the
1621
+ * camera simply stays where it was.
1622
+ ************************************************************/
1623
+ function keep_of(gobj, path)
1624
+ {
1625
+ let priv = gobj.priv;
1626
+
1627
+ if(empty_string(path) || !priv.graph) {
1628
+ return null;
1629
+ }
1630
+ try {
1631
+ for(let node of priv.graph.getNodeData()) {
1632
+ if(node && node.data && node.data.path === path) {
1633
+ let vp = yui_graph_viewport_of(priv.graph, node.id);
1634
+ return vp? {path: path, x: vp[0], y: vp[1]}: null;
1635
+ }
1636
+ }
1637
+ } catch(e) {
1638
+ /* between renders: nothing to hold on to */
1639
+ }
1640
+ return null;
1641
+ }
1642
+
1479
1643
  /************************************************************
1480
1644
  * Clear and reload.
1481
1645
  * {preserve_view: true}
@@ -1538,12 +1702,35 @@ class JsonTreeLayout extends BaseLayout {
1538
1702
  node_dims[node.id] = {w, h};
1539
1703
  }
1540
1704
 
1541
- const H_GAP = 60; // horizontal gap between siblings
1542
- const V_GAP = 50; // vertical gap between levels
1543
1705
 
1544
1706
  /*
1545
1707
  * Calculate subtree widths (bottom-up)
1546
1708
  */
1709
+ /*
1710
+ * A FOLDED node keeps the width its subtree had.
1711
+ *
1712
+ * Without it, folding a branch shrinks its parent's row and
1713
+ * every sibling re-packs -- measured, five surviving cards all
1714
+ * slid 240px sideways for a fold that removed nothing they
1715
+ * could see. Reserving the space trades a hole in the drawing
1716
+ * for a drawing that does not move under the reader, which is
1717
+ * the whole reason to fold something in the first place: to
1718
+ * look at what is left.
1719
+ *
1720
+ * The width comes from the layout's own last pass (stored on
1721
+ * the node below), so a fold and its unfold reserve the same
1722
+ * number and the subtree comes back into the space it left.
1723
+ */
1724
+ /*
1725
+ * A fold leaves a PHANTOM child in the place its children had
1726
+ * -- an invisible node of exactly their width -- so nothing
1727
+ * here needs to know about folding at all. A hole added as a
1728
+ * number would have to be placed too, and `position_node`
1729
+ * centres a parent over its children: a lump appended at the
1730
+ * end moves every sibling by half of it. A child in the
1731
+ * sequence keeps the order and the space, and this stays a
1732
+ * plain tree layout.
1733
+ */
1547
1734
  let subtree_widths = {};
1548
1735
  function calc_subtree_width(node_id) {
1549
1736
  let kids = children_map[node_id] || [];
@@ -1564,6 +1751,18 @@ class JsonTreeLayout extends BaseLayout {
1564
1751
  calc_subtree_width(root.id);
1565
1752
  }
1566
1753
 
1754
+ /*
1755
+ * What this pass measured, written back onto the node data:
1756
+ * it is the only place the number exists, and the gclass needs
1757
+ * it at the moment somebody folds -- by then the layout has
1758
+ * long finished.
1759
+ */
1760
+ for(let node of nodes) {
1761
+ if(node.data) {
1762
+ node.data.subtree_width = subtree_widths[node.id];
1763
+ }
1764
+ }
1765
+
1567
1766
  /*
1568
1767
  * Position nodes (top-down)
1569
1768
  */
@@ -1632,6 +1831,12 @@ class JsonTreeLayout extends BaseLayout {
1632
1831
  /************************************************************
1633
1832
  *
1634
1833
  ************************************************************/
1834
+ function priv_reset_folds(gobj)
1835
+ {
1836
+ gobj.priv.collapsed.clear();
1837
+ gobj.priv.reserved.clear();
1838
+ }
1839
+
1635
1840
  function ac_load_data(gobj, event, kw, src)
1636
1841
  {
1637
1842
  if(kw.path !== undefined) {
@@ -1639,6 +1844,9 @@ function ac_load_data(gobj, event, kw, src)
1639
1844
  }
1640
1845
  if(kw.data !== undefined) {
1641
1846
  gobj_write_attr(gobj, "json_data", kw.data);
1847
+ /* A new document: the folds and the space they were holding
1848
+ * belonged to the old one. */
1849
+ priv_reset_folds(gobj);
1642
1850
  }
1643
1851
 
1644
1852
  refresh_json(gobj);
@@ -1715,12 +1923,41 @@ function ac_toggle_fold(gobj, event, kw, src)
1715
1923
  return -1;
1716
1924
  }
1717
1925
 
1926
+ let card = (kw && kw.keep) || path;
1927
+
1718
1928
  if(priv.collapsed.has(path)) {
1719
1929
  priv.collapsed.delete(path);
1930
+ priv.reserved.delete(path);
1720
1931
  } else {
1932
+ /*
1933
+ * Measured BEFORE folding: afterwards those children are not
1934
+ * there to measure, and this number is the whole of what keeps
1935
+ * every surviving card where it is.
1936
+ *
1937
+ * Held against the CARD that loses them, not against the path
1938
+ * folded: a container with no card of its own hangs its
1939
+ * children from its parent, so its fold is a hole in the
1940
+ * parent's row.
1941
+ */
1942
+ let w = group_width_of(gobj, card, (card === path)? "": path);
1721
1943
  priv.collapsed.add(path);
1944
+ if(w > 0) {
1945
+ priv.reserved.set(path, w);
1946
+ }
1722
1947
  }
1723
1948
 
1949
+ /*
1950
+ * Where the card under the finger is RIGHT NOW.
1951
+ *
1952
+ * `preserve_view` alone keeps the CAMERA, and that is not enough:
1953
+ * folding a branch changes the subtree widths, so the LAYOUT moves
1954
+ * every card under a still camera and the one just clicked slides
1955
+ * out from under the pointer. Read its screen spot here, hand it
1956
+ * back once the new layout is drawn, and everything that did not
1957
+ * move in the layout stays where it was.
1958
+ */
1959
+ priv.pending_keep = keep_of(gobj, (kw && kw.keep) || path);
1960
+
1724
1961
  refresh_json(gobj, {preserve_view: true});
1725
1962
  update_find_result(gobj);
1726
1963
  return 0;
@@ -1755,6 +1992,7 @@ function ac_expand_all(gobj, event, kw, src)
1755
1992
  return 0; // nothing folded; a relayout would only move the camera
1756
1993
  }
1757
1994
  priv.collapsed.clear();
1995
+ priv.reserved.clear(); /* no folds left, so no holes to keep */
1758
1996
  refresh_json(gobj);
1759
1997
  update_find_result(gobj);
1760
1998
  return 0;
@@ -1780,6 +2018,9 @@ function ac_collapse_all(gobj, event, kw, src)
1780
2018
  collect_foldable_paths(json_data, path, paths, true);
1781
2019
 
1782
2020
  priv.collapsed = new Set(paths);
2021
+ /* Collapse-all is asking for a COMPACT drawing, so it reserves
2022
+ * nothing: it packs, and the camera refits with it. */
2023
+ priv.reserved.clear();
1783
2024
  refresh_json(gobj);
1784
2025
  update_find_result(gobj);
1785
2026
  return 0;
package/src/lib_graph.css CHANGED
@@ -273,6 +273,34 @@
273
273
  * wherever a graph is.
274
274
  */
275
275
 
276
+ /*
277
+ * The gobj tree's popover close button, for a FINGER.
278
+ *
279
+ * It is an 18px glyph with 4px of padding either side -- about 26x18
280
+ * of target, which a mouse hits and a thumb does not. The popover
281
+ * opens on a tap, so the thing that closes it has to be reachable by
282
+ * the same thumb that opened it.
283
+ *
284
+ * 44px is the size every touch guideline settles on, and it is applied
285
+ * only under `(pointer: coarse)`: on a mouse the small button is fine
286
+ * and a 44px square in a 26px header row would not be.
287
+ *
288
+ * `!important` because the button carries an inline `cssText` -- a
289
+ * media query has no way to win against one, and the size of a target
290
+ * is not a preference.
291
+ */
292
+ @media (pointer: coarse) {
293
+ .GOBJ_TREE_POPOVER_CLOSE {
294
+ min-width: 44px !important;
295
+ min-height: 44px !important;
296
+ font-size: 26px !important;
297
+ padding: 0 !important;
298
+ display: inline-flex !important;
299
+ align-items: center !important;
300
+ justify-content: center !important;
301
+ }
302
+ }
303
+
276
304
  /*
277
305
  * Picking the node to anchor: the pointer says a click is expected,
278
306
  * which is otherwise announced only by a button at the far end of a
@@ -299,12 +327,20 @@
299
327
  * things down one channel. `outline` draws OUTSIDE the box, so it
300
328
  * cannot move a single row.
301
329
  *
330
+ * DASHED, and the same dash the treedb graph draws inline for its own
331
+ * cards: there, amber already means a find match or a focused topic
332
+ * and blue means the selection, so a third SOLID ring would be a third
333
+ * thing to learn in one channel. A dash is a different channel, and it
334
+ * reads as the provisional thing a camera state is. One feature, one
335
+ * drawing, in all three viewers.
336
+ *
302
337
  * Amber, the same "this one, right now" the toolbar's arming state
303
338
  * uses, and not one of the palette's action colours -- it is not an
304
339
  * action.
305
340
  */
306
- .JSON_CARD_ANCHORED {
307
- outline: 3px solid #fa8c16;
341
+ .JSON_CARD_ANCHORED,
342
+ .GOBJ_CARD_ANCHORED {
343
+ outline: 3px dashed #fa8c16;
308
344
  outline-offset: 2px;
309
345
  }
310
346
 
@@ -235,6 +235,66 @@ export function yui_graph_update_anchor($container, state)
235
235
  ************************************************************/
236
236
  const __centring__ = new WeakMap();
237
237
 
238
+ /************************************************************
239
+ * Put `node_id` at a GIVEN point of the viewport.
240
+ *
241
+ * What "the graph does not jump when I fold something" is
242
+ * made of: read where the card is, rebuild the layout, put
243
+ * that card back where it was. Everything that did not move
244
+ * in the layout stays put with it.
245
+ ************************************************************/
246
+ export async function yui_graph_place_at(graph, node_id, viewport_xy)
247
+ {
248
+ if(!graph || !node_id || !viewport_xy) {
249
+ return false;
250
+ }
251
+ try {
252
+ if(typeof graph.getElementPosition !== "function" ||
253
+ typeof graph.getCanvasCenter !== "function" ||
254
+ typeof graph.translateTo !== "function") {
255
+ return false;
256
+ }
257
+ let z = graph.getZoom() || 1;
258
+ let c = graph.getCanvasCenter();
259
+ let n = graph.getElementPosition(node_id);
260
+
261
+ /*
262
+ * Same derivation as the centring below, with the target point
263
+ * left free instead of fixed at the middle:
264
+ *
265
+ * T = (canvasCentre - node) * zoom + (V - canvasCentre)
266
+ *
267
+ * and putting V = canvasCentre gives the centring exactly.
268
+ */
269
+ await graph.translateTo([
270
+ (c[0] - n[0]) * z + (viewport_xy[0] - c[0]),
271
+ (c[1] - n[1]) * z + (viewport_xy[1] - c[1])
272
+ ]);
273
+ return true;
274
+ } catch(e) {
275
+ return false; /* the element is gone, or the graph is between renders */
276
+ }
277
+ }
278
+
279
+ /************************************************************
280
+ * Where `node_id` is right now, in viewport pixels.
281
+ *
282
+ * The pair of the call above: read it before a rebuild, hand
283
+ * it back after, and the node has not moved on screen even
284
+ * though the layout under it changed.
285
+ ************************************************************/
286
+ export function yui_graph_viewport_of(graph, node_id)
287
+ {
288
+ if(!graph || !node_id) {
289
+ return null;
290
+ }
291
+ try {
292
+ return graph.getViewportByCanvas(graph.getElementPosition(node_id));
293
+ } catch(e) {
294
+ return null;
295
+ }
296
+ }
297
+
238
298
  export async function yui_graph_center_on(graph, node_id)
239
299
  {
240
300
  if(!graph || !node_id) {