@yuneta/gobj-ui 7.23.158 → 7.23.160

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.158",
3
+ "version": "7.23.160",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_form.js CHANGED
@@ -61,7 +61,8 @@ import {
61
61
  yui_file_control
62
62
  } from "./yui_file_field.js";
63
63
  import {
64
- yui_asset_id
64
+ yui_asset_id,
65
+ yui_asset_release
65
66
  } from "./yui_asset.js";
66
67
 
67
68
  import "./c_yui_form.css";
@@ -192,6 +193,9 @@ function mt_create(gobj)
192
193
  ************************************************************/
193
194
  function mt_destroy(gobj)
194
195
  {
196
+ /* A file preview holds blob urls, which keep their bytes alive
197
+ * until revoked -- and removing the DOM revokes nothing. */
198
+ yui_asset_release(gobj_read_attr(gobj, "$container"));
195
199
  destroy_ui(gobj);
196
200
  }
197
201
 
@@ -3155,6 +3159,51 @@ function ac_clear_record(gobj, event, kw, src)
3155
3159
  return 0;
3156
3160
  }
3157
3161
 
3162
+ /************************************************************
3163
+ * From the host: what it fetched for the asset a `file` column
3164
+ * names -- the `get-asset` answer, or why there is none.
3165
+ * {
3166
+ * name: the column
3167
+ * id: the asset id the answer is for
3168
+ * answer: the get-asset answer, or null
3169
+ * error: why there is no answer, when there is none
3170
+ * }
3171
+ *
3172
+ * Kept on the control and drawn only while the column still names
3173
+ * THAT id: an answer landing after the person picked another file
3174
+ * or cleared the column must not bring the old picture back.
3175
+ ************************************************************/
3176
+ function ac_set_file_preview(gobj, event, kw, src)
3177
+ {
3178
+ let $container = gobj_read_attr(gobj, "$container");
3179
+ let $form = $container? $container.querySelector('form'): null;
3180
+ if(!$form) {
3181
+ log_error(`${gobj_short_name(gobj)}: no form to show the file preview in`);
3182
+ return -1;
3183
+ }
3184
+
3185
+ let $file = null;
3186
+ $form.querySelectorAll('.FILE_FIELD').forEach(($el) => {
3187
+ if($el.dataset.treedb_name === kw.name) {
3188
+ $file = $el;
3189
+ }
3190
+ });
3191
+ if(!$file) {
3192
+ log_error(`${gobj_short_name(gobj)}: no file column '${kw.name}' in the form`);
3193
+ return -1;
3194
+ }
3195
+
3196
+ $file.yui_file_stored = {
3197
+ id: kw.id || "",
3198
+ answer: kw.answer || null,
3199
+ error: kw.error || ""
3200
+ };
3201
+ if($file.yui_file_render) {
3202
+ $file.yui_file_render();
3203
+ }
3204
+ return 0;
3205
+ }
3206
+
3158
3207
  /************************************************************
3159
3208
  * From internal form button for new rows
3160
3209
  * or from load_tabulator_data() to load data from backend
@@ -3390,6 +3439,7 @@ function create_gclass(gclass_name)
3390
3439
  ["EV_RECORD_CHANGED", ac_record_changed, null],
3391
3440
  ["EV_COPY_RECORD", ac_copy_record, null],
3392
3441
  ["EV_PASTE_RECORD", ac_paste_record, null],
3442
+ ["EV_SET_FILE_PREVIEW", ac_set_file_preview, null],
3393
3443
 
3394
3444
  ["EV_WINDOW_MOVED", null, null],
3395
3445
  ["EV_WINDOW_RESIZED", null, null],
@@ -3413,6 +3463,7 @@ function create_gclass(gclass_name)
3413
3463
  ["EV_RECORD_CHANGED", 0],
3414
3464
  ["EV_COPY_RECORD", 0],
3415
3465
  ["EV_PASTE_RECORD", 0],
3466
+ ["EV_SET_FILE_PREVIEW", 0],
3416
3467
 
3417
3468
  ["EV_WINDOW_MOVED", 0],
3418
3469
  ["EV_WINDOW_RESIZED", 0],
@@ -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(
@@ -141,3 +141,70 @@
141
141
  font-style: italic;
142
142
  opacity: 0.6;
143
143
  }
144
+
145
+ /*
146
+ * A file cell that names an asset is a LINK: the click opens it
147
+ * (EV_SHOW_CELL_FILE). Same shape as the JSON cell beside it.
148
+ */
149
+ .FILE_CELL_LINK {
150
+ white-space: nowrap;
151
+ }
152
+ .FILE_CELL_LINK .FILE_CELL_ICON {
153
+ margin-right: 0.25rem;
154
+ vertical-align: middle;
155
+ }
156
+ .FILE_CELL_LINK .FILE_CELL_ID {
157
+ text-decoration: underline;
158
+ vertical-align: middle;
159
+ }
160
+ .FILE_CELL_LINK .FILE_CELL_MORE {
161
+ margin-left: 0.35rem;
162
+ font-family: inherit;
163
+ opacity: 0.7;
164
+ vertical-align: middle;
165
+ }
166
+
167
+ /*
168
+ * The file popup: one item per asset the cell names, each the thing
169
+ * itself and a caption (what it is, and a way to open it full size).
170
+ */
171
+ .TREEDB_CELL_FILE {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 1rem;
175
+ padding: 0.75rem;
176
+ height: 100%;
177
+ overflow: auto;
178
+ }
179
+ .TREEDB_CELL_FILE .CELL_FILE_ITEM {
180
+ display: flex;
181
+ flex-direction: column;
182
+ align-items: center;
183
+ gap: 0.35rem;
184
+ min-width: 0;
185
+ }
186
+ .TREEDB_CELL_FILE img.CELL_FILE_MEDIA,
187
+ .TREEDB_CELL_FILE video.CELL_FILE_MEDIA {
188
+ display: block;
189
+ max-width: 100%;
190
+ max-height: 70vh;
191
+ object-fit: contain;
192
+ }
193
+ .TREEDB_CELL_FILE audio.CELL_FILE_MEDIA {
194
+ width: 100%;
195
+ }
196
+ .TREEDB_CELL_FILE iframe.CELL_FILE_MEDIA {
197
+ min-height: 60vh;
198
+ }
199
+ .TREEDB_CELL_FILE .CELL_FILE_CAPTION {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ justify-content: center;
203
+ gap: 0.75rem;
204
+ font-size: 0.8rem;
205
+ opacity: 0.85;
206
+ }
207
+ .TREEDB_CELL_FILE .CELL_FILE_ID {
208
+ font-family: monospace;
209
+ overflow-wrap: anywhere;
210
+ }