@yuneta/gobj-ui 7.23.159 → 7.23.161

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.
@@ -25,6 +25,12 @@
25
25
  * via the host-supplied `node_route`). An alternate landing to the
26
26
  * topic cards, in the spirit of "every treedb is a graph".
27
27
  *
28
+ * The CAMERA is the family's (yui_graph_camera.js): the same
29
+ * toolbar cluster -- zoom in/out, readout, fit, 1:1 -- and the
30
+ * same wheel: it SCROLLS, and Ctrl + wheel zooms, over the cards
31
+ * too (yui_graph_forward_wheel). This view zoomed on a bare wheel
32
+ * and had no toolbar, beside graphs that did neither.
33
+ *
28
34
  * Copyright (c) 2026, ArtGins.
29
35
  * All Rights Reserved.
30
36
  ***********************************************************************/
@@ -49,10 +55,20 @@ import {
49
55
  gobj_read_str_attr,
50
56
  is_object,
51
57
  escapeHtml,
58
+ refresh_language,
52
59
  } from "@yuneta/gobj-js";
53
60
 
54
61
  import {Graph, NodeEvent} from "@antv/g6";
55
62
 
63
+ import {yui_toolbar} from "./yui_toolbar.js";
64
+ import {
65
+ yui_graph_camera_items,
66
+ yui_graph_camera_behaviors,
67
+ yui_graph_forward_wheel,
68
+ yui_graph_update_zoom,
69
+ } from "./yui_graph_camera.js";
70
+ import {ensure_pinch_zoom_patch} from "./g6_touch_gestures.js";
71
+
56
72
  import {getStrokeColor} from "./lib_graph.js";
57
73
 
58
74
  import {yui_is_dark, yui_watch_theme} from "./yui_theme.js";
@@ -98,14 +114,17 @@ SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Treedb schema: {top
98
114
  SDATA(data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
99
115
  SDATA(data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, "With no `node_route`, PUBLISH a node click as EV_NODE_CLICK instead of dropping it. Opt-in, because a subscriber has to DECLARE the event: the hosts that route by hash, and the ones that want nothing, must not have it appear underneath them"),
100
116
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
117
+ SDATA(data_type_t.DTP_STRING, "wide", 0, "40px", "Height of the toolbar buttons: the family's (C_YUI_TREEDB_GRAPH, C_YUI_JSON_GRAPH, C_YUI_GOBJ_TREE_JS)"),
101
118
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
102
119
  SDATA_END()
103
120
  ];
104
121
 
105
122
  let PRIVATE_DATA = {
106
123
  $container: null,
124
+ $canvas: null, // the element G6 is mounted in, under the toolbar
107
125
  graph: null,
108
126
  theme_observer: null, // MutationObserver on <html data-theme>
127
+ unforward_wheel: null, // removes the wheel forwarding of the cards
109
128
  };
110
129
 
111
130
  let __gclass__ = null;
@@ -122,6 +141,9 @@ let __gclass__ = null;
122
141
 
123
142
  function mt_create(gobj)
124
143
  {
144
+ /* Pinch to zoom on a touch screen, as in the other graphs. */
145
+ ensure_pinch_zoom_patch();
146
+
125
147
  build_ui(gobj);
126
148
 
127
149
  /*
@@ -179,9 +201,31 @@ function mt_destroy(gobj)
179
201
  ************************************************************/
180
202
  function build_ui(gobj)
181
203
  {
204
+ let priv = gobj.priv;
205
+ let wide = gobj_read_str_attr(gobj, "wide");
206
+
207
+ /* The camera cluster and nothing else: this view has no folding,
208
+ * no layouts and no find -- a schema is a handful of cards. */
209
+ const $toolbar = yui_toolbar({}, [
210
+ ['div', {class: 'yui-horizontal-toolbar-section left'}, []],
211
+ ['div', {class: 'yui-horizontal-toolbar-section center'},
212
+ yui_graph_camera_items(gobj, null, wide)],
213
+ ['div', {class: 'yui-horizontal-toolbar-section right'}, []],
214
+ ]);
215
+ refresh_language($toolbar, t);
216
+
217
+ priv.$canvas = createElement2(
218
+ ['div', {class: 'TREEDB_SCHEMA_CANVAS',
219
+ style: 'position:relative; flex:1 1 auto; min-height:0;'}, []]
220
+ );
221
+
182
222
  let $container = createElement2(
183
223
  ['div', {class: 'C_YUI_TREEDB_SCHEMA TREEDB_SCHEMA_VIEW',
184
- style: 'position:relative; height:100%; min-height:0;'}, []]
224
+ style: 'position:relative; height:100%; min-height:0; ' +
225
+ 'display:flex; flex-direction:column;'}, [
226
+ ['div', {class: 'TREEDB_SCHEMA_TOOLBAR', style: 'flex:0 0 auto;'}, [$toolbar]],
227
+ priv.$canvas
228
+ ]]
185
229
  );
186
230
  gobj_write_attr(gobj, "$container", $container);
187
231
  }
@@ -550,7 +594,8 @@ function build_graph(gobj)
550
594
  {
551
595
  let priv = gobj.priv;
552
596
  let $container = gobj_read_attr(gobj, "$container");
553
- if(!$container) {
597
+ if(!$container || !priv.$canvas) {
598
+ log_error(`${gobj_short_name(gobj)}: no UI to draw the schema in`);
554
599
  return;
555
600
  }
556
601
  destroy_graph(gobj);
@@ -561,7 +606,7 @@ function build_graph(gobj)
561
606
  ['div', {class: 'TREEDB_SCHEMA_EMPTY p-4 yui-text-quiet',
562
607
  i18n: 'no topics'}, t('no topics', {defaultValue: 'No topics'})]
563
608
  );
564
- $container.appendChild($empty);
609
+ priv.$canvas.appendChild($empty);
565
610
  return;
566
611
  }
567
612
 
@@ -569,8 +614,9 @@ function build_graph(gobj)
569
614
  let graph;
570
615
  try {
571
616
  graph = new Graph({
572
- container: $container,
617
+ container: priv.$canvas,
573
618
  autoResize: true,
619
+ animation: false,
574
620
  data: data,
575
621
  edge: {
576
622
  style: edge_style(dark),
@@ -581,7 +627,8 @@ function build_graph(gobj)
581
627
  nodesep: 28,
582
628
  ranksep: 90,
583
629
  },
584
- behaviors: ["zoom-canvas", "drag-canvas", "drag-element"],
630
+ /* The family's camera: the wheel scrolls, Ctrl + wheel zooms. */
631
+ behaviors: ["drag-canvas", ...yui_graph_camera_behaviors(), "drag-element"],
585
632
  });
586
633
  } catch(e) {
587
634
  log_error(`${gobj_short_name(gobj)}: schema graph create failed: ${e}`);
@@ -591,6 +638,16 @@ function build_graph(gobj)
591
638
 
592
639
  graph.setTheme(dark ? "dark" : "light");
593
640
 
641
+ /* The readout follows ANY camera change, the wheel included -- a
642
+ * wheel notch passes through no action of ours. */
643
+ graph.on('aftertransform', () => {
644
+ yui_graph_update_zoom($container, priv.graph);
645
+ });
646
+
647
+ /* The cards cover most of this drawing, and a card does not pass
648
+ * the wheel on: without this it works only between them. */
649
+ priv.unforward_wheel = yui_graph_forward_wheel(graph, priv.$canvas);
650
+
594
651
  /* A node click opens that topic — a real hash navigation, so it is
595
652
  * deep-linkable and Back-friendly like the cards. Crosses the FSM. */
596
653
  graph.on(NodeEvent.CLICK, (evt) => {
@@ -601,7 +658,9 @@ function build_graph(gobj)
601
658
  /* No fitView: the diagram is drawn at its own scale and stays there.
602
659
  * Zooming it to the container the moment it appears rewrites the size
603
660
  * the reader just saw, and does it differently for every treedb. */
604
- graph.render().catch((e) => {
661
+ graph.render().then(() => {
662
+ yui_graph_update_zoom($container, priv.graph);
663
+ }).catch((e) => {
605
664
  log_error(`${gobj_short_name(gobj)}: schema graph render failed: ${e}`);
606
665
  });
607
666
  }
@@ -612,6 +671,10 @@ function build_graph(gobj)
612
671
  function destroy_graph(gobj)
613
672
  {
614
673
  let priv = gobj.priv;
674
+ if(priv.unforward_wheel) {
675
+ priv.unforward_wheel();
676
+ priv.unforward_wheel = null;
677
+ }
615
678
  if(priv.graph) {
616
679
  try {
617
680
  priv.graph.destroy();
@@ -620,10 +683,10 @@ function destroy_graph(gobj)
620
683
  }
621
684
  priv.graph = null;
622
685
  }
623
- let $container = gobj_read_attr(gobj, "$container");
624
- if($container) {
625
- while($container.firstChild) {
626
- $container.removeChild($container.firstChild);
686
+ /* The CANVAS only: the toolbar is built once and stays. */
687
+ if(priv.$canvas) {
688
+ while(priv.$canvas.firstChild) {
689
+ priv.$canvas.removeChild(priv.$canvas.firstChild);
627
690
  }
628
691
  }
629
692
  }
@@ -753,6 +816,59 @@ function ac_theme(gobj, event, kw, src)
753
816
  return 0;
754
817
  }
755
818
 
819
+ /************************************************************
820
+ * The camera, from the toolbar. Same moves as the family's
821
+ * (C_YUI_GOBJ_TREE_JS): +/- 20%, fit, and actual size at the
822
+ * layout's origin.
823
+ ************************************************************/
824
+ function camera_move(gobj, what, promise)
825
+ {
826
+ Promise.resolve(promise).then(() => {
827
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), gobj.priv.graph);
828
+ }).catch((e) => {
829
+ log_error(`${gobj_short_name(gobj)}: schema graph ${what} failed: ${e}`);
830
+ });
831
+ }
832
+
833
+ function ac_zoom_in(gobj, event, kw, src)
834
+ {
835
+ let graph = gobj.priv.graph;
836
+ if(graph) {
837
+ camera_move(gobj, "zoom in", graph.zoomTo(graph.getZoom() * 1.2));
838
+ }
839
+ return 0;
840
+ }
841
+
842
+ function ac_zoom_out(gobj, event, kw, src)
843
+ {
844
+ let graph = gobj.priv.graph;
845
+ if(graph) {
846
+ camera_move(gobj, "zoom out", graph.zoomTo(graph.getZoom() * 0.8));
847
+ }
848
+ return 0;
849
+ }
850
+
851
+ function ac_zoom_reset(gobj, event, kw, src)
852
+ {
853
+ let graph = gobj.priv.graph;
854
+ if(graph && graph.rendered) {
855
+ camera_move(gobj, "actual size",
856
+ Promise.resolve(graph.zoomTo(1)).then(() => graph.translateTo([0, 0])));
857
+ }
858
+ return 0;
859
+ }
860
+
861
+ /* A fit ASKED for is fine: what this view refuses is fitting on its
862
+ * own, the moment it appears (see build_graph). */
863
+ function ac_center(gobj, event, kw, src)
864
+ {
865
+ let graph = gobj.priv.graph;
866
+ if(graph) {
867
+ camera_move(gobj, "fit", graph.fitView());
868
+ }
869
+ return 0;
870
+ }
871
+
756
872
  /************************************************************
757
873
  * Rebuild from a fresh schema (descs arrived / changed).
758
874
  ************************************************************/
@@ -795,6 +911,10 @@ function create_gclass(gclass_name)
795
911
  ["EV_SHOW", ac_show, null],
796
912
  ["EV_THEME", ac_theme, null],
797
913
  ["EV_REBUILD", ac_rebuild, null],
914
+ ["EV_ZOOM_IN", ac_zoom_in, null],
915
+ ["EV_ZOOM_OUT", ac_zoom_out, null],
916
+ ["EV_ZOOM_RESET", ac_zoom_reset, null],
917
+ ["EV_CENTER", ac_center, null],
798
918
  ]]
799
919
  ];
800
920
 
@@ -805,6 +925,10 @@ function create_gclass(gclass_name)
805
925
  ["EV_SHOW", 0],
806
926
  ["EV_THEME", 0],
807
927
  ["EV_REBUILD", 0],
928
+ ["EV_ZOOM_IN", 0],
929
+ ["EV_ZOOM_OUT", 0],
930
+ ["EV_ZOOM_RESET", 0],
931
+ ["EV_CENTER", 0],
808
932
  ];
809
933
 
810
934
  __gclass__ = gclass_create(
@@ -2928,13 +2928,19 @@ function show_edit_form(gobj, row, index)
2928
2928
  * Does a click on this column already mean something else?
2929
2929
  *
2930
2930
  * The `_` columns are the table's own scaffolding (the selection
2931
- * checkbox, the Op buttons); a hook opens its children and a col
2932
- * holding a json document opens it in a viewer. Anything else is
2933
- * plain data, and a click on it means "show me this record".
2931
+ * checkbox, the Op buttons); a hook opens its children, a col
2932
+ * holding a json document opens it in a viewer, and a `file` col
2933
+ * opens its asset in a popup. Anything else is plain data, and a
2934
+ * click on it means "show me this record".
2935
+ *
2936
+ * `file` was missing when its popup arrived (7.23.159), so a click
2937
+ * on a photo opened the popup AND the record on top of it: Tabulator
2938
+ * fires `rowClick` from its own dispatcher, and the cell's
2939
+ * stopPropagation() does not reach it.
2934
2940
  ************************************************************/
2935
2941
  const CELL_WITH_ACTION = [
2936
2942
  "hook", "object", "dict", "template", "array", "list",
2937
- "coordinates", "blob", "gbuffer"
2943
+ "coordinates", "blob", "gbuffer", "file"
2938
2944
  ];
2939
2945
 
2940
2946
  function cell_has_its_own_action(gobj, field)
@@ -149,6 +149,70 @@ export function yui_graph_camera_behaviors()
149
149
  ];
150
150
  }
151
151
 
152
+ /************************************************************
153
+ * Make the wheel work over HTML nodes too.
154
+ *
155
+ * G6's HTML nodes live in a DOM layer BESIDE the canvas
156
+ * element, not in it, and they forward six pointer events and
157
+ * not the wheel. `scroll-canvas` listens on the canvas element
158
+ * and `zoom-canvas` reads G's own wheel event, which is fed
159
+ * from that same element -- so a wheel over a card reaches
160
+ * neither, and on a graph drawn with big cards the wheel only
161
+ * works in the gaps between them.
162
+ *
163
+ * The wheel is caught on `$host` (the element G6 was mounted
164
+ * in) and dispatched again ON the canvas element, carrying the
165
+ * deltas, the position and the modifiers: both behaviors then
166
+ * take it exactly as a wheel over empty canvas. Control is
167
+ * read by G6 from the real keydown, which is still held.
168
+ *
169
+ * A wheel whose target is the canvas passes untouched, which
170
+ * is also what keeps the re-dispatched one from looping. An
171
+ * element that must scroll by itself (a popover) stops the
172
+ * wheel before it gets here.
173
+ *
174
+ * Returns the function that removes the listener.
175
+ ************************************************************/
176
+ export function yui_graph_forward_wheel(graph, $host)
177
+ {
178
+ if(!graph || !$host) {
179
+ return () => {};
180
+ }
181
+ const on_wheel = (e) => {
182
+ let dom = null;
183
+ try {
184
+ dom = graph.getCanvas().getContextService().getDomElement();
185
+ } catch(err) {
186
+ return; /* the graph is gone: nothing to hand it to */
187
+ }
188
+ if(!dom || e.target === dom) {
189
+ return;
190
+ }
191
+ e.preventDefault();
192
+ e.stopPropagation();
193
+ dom.dispatchEvent(new WheelEvent("wheel", {
194
+ deltaX: e.deltaX,
195
+ deltaY: e.deltaY,
196
+ deltaZ: e.deltaZ,
197
+ deltaMode: e.deltaMode,
198
+ clientX: e.clientX,
199
+ clientY: e.clientY,
200
+ screenX: e.screenX,
201
+ screenY: e.screenY,
202
+ ctrlKey: e.ctrlKey,
203
+ shiftKey: e.shiftKey,
204
+ altKey: e.altKey,
205
+ metaKey: e.metaKey,
206
+ bubbles: true,
207
+ cancelable: true,
208
+ }));
209
+ };
210
+ $host.addEventListener("wheel", on_wheel, {passive: false});
211
+ return () => {
212
+ $host.removeEventListener("wheel", on_wheel);
213
+ };
214
+ }
215
+
152
216
  /************************************************************
153
217
  * The camera cluster: zoom in, zoom out, readout, fit,
154
218
  * actual size. Returns createElement2 specs, in order.