@yuneta/gobj-ui 7.23.3 → 7.23.4

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/README.md CHANGED
@@ -448,12 +448,16 @@ stays. Note the close contract: `C_YUI_WINDOW.close_window()` calls `on_close`
448
448
  and THEN destroys itself, so a host must drop its reference rather than destroy
449
449
  the window too.
450
450
 
451
- Its camera speaks the **same vocabulary as the treedb graph** (`7.23.3`): zoom
452
- in / zoom out, a live **zoom readout**, `yi-fit` for fit — the same corner
453
- brackets the SVG sprite draws for `g6-icon-fit` — and the WRITTEN `1:1` for
454
- actual size, which is written in every editor that offers it and never drawn.
455
- The readout hangs off G6's `aftertransform`, the one hook that also covers the
456
- wheel.
451
+ Its camera comes from **`yui_graph_camera.js`** (`7.23.4`), the one place every
452
+ DOM-toolbar graph asks for one — this gclass and the gobj tree both, so a
453
+ cloned toolbar cannot drift apart again. The vocabulary is the treedb graph's
454
+ (`c_g6_nodes_tree.js`, which draws its toolbar as a G6 plugin over an SVG
455
+ sprite and so cannot share the code, only the decisions): zoom in / zoom out, a
456
+ live **zoom readout**, `yi-fit` for fit — the same corner brackets the sprite
457
+ draws for `g6-icon-fit` — and the WRITTEN `1:1` for actual size, which is
458
+ written in every editor that offers it and never drawn. Fold is the chevron
459
+ pair, from the lazy tree viewer's own toolbar. The readout hangs off G6's
460
+ `aftertransform`, the one hook that also covers the wheel.
457
461
 
458
462
  The graph carries **its own** toolbar, a **layout picker**
459
463
  (`EV_CHANGE_LAYOUT {layout}`: `vertical tree`, `dagre top-down`,
@@ -9350,6 +9350,160 @@ function yui_toolbar(attrs = {}, items = []) {
9350
9350
  return $toolbar;
9351
9351
  }
9352
9352
  //#endregion
9353
+ //#region src/yui_graph_camera.js
9354
+ /***********************************************************************
9355
+ * yui_graph_camera.js
9356
+ *
9357
+ * The camera and fold controls a G6 graph puts in a DOM toolbar,
9358
+ * built in ONE place.
9359
+ *
9360
+ * This module exists because the same action was drawn three
9361
+ * different ways across sibling views — actual size as a bare
9362
+ * magnifier here and as `1:1` there, fit as `arrows-to-eye` here and
9363
+ * as corner brackets there, fold as chevrons here and as an eye
9364
+ * there. A user sees two of these graphs side by side in the same
9365
+ * console, so a copied-and-drifted toolbar is not a detail.
9366
+ *
9367
+ * The VOCABULARY is not invented here. It is the one
9368
+ * `c_g6_nodes_tree.js` settled on for the treedb graph, which draws
9369
+ * its toolbar as a G6 plugin over an SVG sprite and therefore cannot
9370
+ * share this code — only its decisions:
9371
+ *
9372
+ * zoom in / zoom out magnifiers
9373
+ * zoom level a READOUT, because without it "1:1" is
9374
+ * a jump to a value nobody was told
9375
+ * fit corner brackets (`yi-fit`, the same
9376
+ * path as the sprite's `g6-icon-fit`)
9377
+ * actual size the WRITTEN `1:1` — the action is
9378
+ * `zoomTo(1)`, and actual size is written
9379
+ * in every editor that offers it, never
9380
+ * drawn, because there is no glyph for it
9381
+ *
9382
+ * Fold is the one the treedb graph does not have, so its vocabulary
9383
+ * comes from the other side of the family: the lazy tree viewer's own
9384
+ * toolbar (`c_yui_json.js`) uses chevrons — open pointing down, closed
9385
+ * pointing right — which is also the direction the per-node handles
9386
+ * speak. An eye means show/hide, which is a different idea.
9387
+ *
9388
+ * Copyright (c) 2026, ArtGins.
9389
+ * All Rights Reserved.
9390
+ ***********************************************************************/
9391
+ /************************************************************
9392
+ * One icon button that fires `event_name`.
9393
+ ************************************************************/
9394
+ function camera_button(gobj, icon, event_name, label_key, wide, extra_style) {
9395
+ return [
9396
+ "button",
9397
+ {
9398
+ class: `button ${event_name}`,
9399
+ type: "button",
9400
+ style: {
9401
+ height: wide,
9402
+ width: "2.5em"
9403
+ },
9404
+ title: (0, i18next.t)(label_key),
9405
+ "data-i18n-title": label_key,
9406
+ "aria-label": (0, i18next.t)(label_key),
9407
+ "data-i18n-aria-label": label_key
9408
+ },
9409
+ ["i", {
9410
+ style: "font-size:1.5em; color:inherit;" + (extra_style || ""),
9411
+ class: icon
9412
+ }],
9413
+ { click: (evt) => {
9414
+ evt.stopPropagation();
9415
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, event_name, { evt }, gobj);
9416
+ } }
9417
+ ];
9418
+ }
9419
+ /************************************************************
9420
+ * The current zoom as a percentage.
9421
+ ************************************************************/
9422
+ function yui_graph_zoom_text(graph) {
9423
+ if (!graph) return "100%";
9424
+ try {
9425
+ return Math.round(graph.getZoom() * 100) + "%";
9426
+ } catch (e) {
9427
+ return "100%";
9428
+ }
9429
+ }
9430
+ /************************************************************
9431
+ * Repaint the readout of a container that carries one.
9432
+ *
9433
+ * Call it from G6's `aftertransform`, which is the ONE hook
9434
+ * that also covers the WHEEL — a camera change that passes
9435
+ * through no action, so repainting from each zoom action
9436
+ * leaves the number lying after every notch.
9437
+ ************************************************************/
9438
+ function yui_graph_update_zoom($container, graph) {
9439
+ if (!$container) return;
9440
+ let $readout = $container.querySelector(".GRAPH_ZOOM_LEVEL");
9441
+ if (!$readout) return;
9442
+ $readout.textContent = yui_graph_zoom_text(graph);
9443
+ }
9444
+ /************************************************************
9445
+ * The camera cluster: zoom in, zoom out, readout, fit,
9446
+ * actual size. Returns createElement2 specs, in order.
9447
+ *
9448
+ * `wide` is the host toolbar's button height.
9449
+ ************************************************************/
9450
+ function yui_graph_camera_items(gobj, graph, wide) {
9451
+ return [
9452
+ camera_button(gobj, "yi-magnifying-glass-plus", "EV_ZOOM_IN", "zoom in", wide),
9453
+ camera_button(gobj, "yi-magnifying-glass-minus", "EV_ZOOM_OUT", "zoom out", wide),
9454
+ [
9455
+ "span",
9456
+ {
9457
+ class: "GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey",
9458
+ style: "font-size:.85rem; min-width:3.5em; justify-content:center;",
9459
+ title: (0, i18next.t)("zoom level"),
9460
+ "data-i18n-title": "zoom level"
9461
+ },
9462
+ yui_graph_zoom_text(graph)
9463
+ ],
9464
+ camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", wide),
9465
+ [
9466
+ "button",
9467
+ {
9468
+ class: "button EV_ZOOM_RESET",
9469
+ type: "button",
9470
+ style: {
9471
+ height: wide,
9472
+ width: "2.5em"
9473
+ },
9474
+ title: (0, i18next.t)("actual size"),
9475
+ "data-i18n-title": "actual size",
9476
+ "aria-label": (0, i18next.t)("actual size"),
9477
+ "data-i18n-aria-label": "actual size"
9478
+ },
9479
+ [
9480
+ "span",
9481
+ { style: "font-weight:700;" },
9482
+ "1:1"
9483
+ ],
9484
+ { click: (evt) => {
9485
+ evt.stopPropagation();
9486
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ZOOM_RESET", { evt }, gobj);
9487
+ } }
9488
+ ]
9489
+ ];
9490
+ }
9491
+ /************************************************************
9492
+ * The fold pair: expand all, collapse all. Chevrons, the
9493
+ * open one rotated down — the same pair the lazy tree
9494
+ * viewer's toolbar uses, and the same direction the
9495
+ * per-node handles speak.
9496
+ ************************************************************/
9497
+ function yui_graph_fold_items(gobj, wide) {
9498
+ return [camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide, " transform: rotate(90deg);"), camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide)];
9499
+ }
9500
+ /************************************************************
9501
+ * A refresh button, for the graphs that offer one.
9502
+ ************************************************************/
9503
+ function yui_graph_refresh_item(gobj, wide) {
9504
+ return camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", wide);
9505
+ }
9506
+ //#endregion
9353
9507
  //#region src/g6_drag_canvas_touch.js
9354
9508
  /***********************************************************************
9355
9509
  * g6_drag_canvas_touch.js
@@ -9556,7 +9710,6 @@ var PRIVATE_DATA$11 = {
9556
9710
  find_timer: null,
9557
9711
  fold_listener: null,
9558
9712
  $layout_select: null,
9559
- $zoom_readout: null,
9560
9713
  $find_input: null,
9561
9714
  $find_result: null,
9562
9715
  $find_count: null
@@ -9754,84 +9907,6 @@ function destroy_ui$6(gobj) {
9754
9907
  }
9755
9908
  }
9756
9909
  /************************************************************
9757
- * One camera button.
9758
- ************************************************************/
9759
- function camera_button(gobj, icon, event_name, label_key, wide) {
9760
- return [
9761
- "button",
9762
- {
9763
- class: `button ${event_name}`,
9764
- type: "button",
9765
- style: {
9766
- height: wide,
9767
- width: "2.5em"
9768
- },
9769
- title: (0, i18next.t)(label_key),
9770
- "data-i18n-title": label_key,
9771
- "aria-label": (0, i18next.t)(label_key),
9772
- "data-i18n-aria-label": label_key
9773
- },
9774
- ["i", {
9775
- style: "font-size:1.5em; color:inherit;",
9776
- class: icon
9777
- }],
9778
- { click: (evt) => {
9779
- evt.stopPropagation();
9780
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, event_name, { evt }, gobj);
9781
- } }
9782
- ];
9783
- }
9784
- /************************************************************
9785
- * The zoom, as a percentage. What the two magnifiers
9786
- * change: an editor that offers a zoom shows the number.
9787
- ************************************************************/
9788
- function zoom_percent_text$1(gobj) {
9789
- let graph = gobj.priv.graph;
9790
- if (!graph) return "100%";
9791
- try {
9792
- return Math.round(graph.getZoom() * 100) + "%";
9793
- } catch (e) {
9794
- return "100%";
9795
- }
9796
- }
9797
- /************************************************************
9798
- * Repaint the readout after anything that moves the camera.
9799
- ************************************************************/
9800
- function update_zoom_readout$1(gobj) {
9801
- let priv = gobj.priv;
9802
- if (priv.$zoom_readout) priv.$zoom_readout.textContent = zoom_percent_text$1(gobj);
9803
- }
9804
- /************************************************************
9805
- * One fold button. `open` rotates the chevron to the open
9806
- * state, mirroring the per-node toggle of the lazy tree.
9807
- ************************************************************/
9808
- function graph_fold_button(gobj, event_name, label_key, open) {
9809
- let icon_style = "font-size:1.5em; color:inherit;" + (open ? " transform: rotate(90deg);" : "");
9810
- return [
9811
- "button",
9812
- {
9813
- class: `button ${event_name}`,
9814
- type: "button",
9815
- style: {
9816
- height: (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "wide"),
9817
- width: "2.5em"
9818
- },
9819
- title: (0, i18next.t)(label_key),
9820
- "data-i18n-title": label_key,
9821
- "aria-label": (0, i18next.t)(label_key),
9822
- "data-i18n-aria-label": label_key
9823
- },
9824
- ["i", {
9825
- style: icon_style,
9826
- class: "yi-chevron-right"
9827
- }],
9828
- { click: (evt) => {
9829
- evt.stopPropagation();
9830
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, event_name, {}, gobj);
9831
- } }
9832
- ];
9833
- }
9834
- /************************************************************
9835
9910
  * Toolbar
9836
9911
  ************************************************************/
9837
9912
  function make_toolbar$3(gobj) {
@@ -9881,52 +9956,8 @@ function make_toolbar$3(gobj) {
9881
9956
  ]]
9882
9957
  ]];
9883
9958
  let center_items = [];
9884
- for (let item of [[
9885
- "yi-magnifying-glass-plus",
9886
- "EV_ZOOM_IN",
9887
- "zoom in"
9888
- ], [
9889
- "yi-magnifying-glass-minus",
9890
- "EV_ZOOM_OUT",
9891
- "zoom out"
9892
- ]]) center_items.push(camera_button(gobj, item[0], item[1], item[2], toolbar_wide));
9893
- priv.$zoom_readout = (0, _yuneta_gobj_js.createElement2)([
9894
- "span",
9895
- {
9896
- class: "JSON_GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey",
9897
- style: "font-size:.85rem; min-width:3.5em; justify-content:center;",
9898
- title: (0, i18next.t)("zoom level"),
9899
- "data-i18n-title": "zoom level"
9900
- },
9901
- zoom_percent_text$1(gobj)
9902
- ]);
9903
- center_items.push(priv.$zoom_readout);
9904
- center_items.push(camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", toolbar_wide));
9905
- center_items.push([
9906
- "button",
9907
- {
9908
- class: "button EV_ZOOM_RESET",
9909
- type: "button",
9910
- style: {
9911
- height: toolbar_wide,
9912
- width: "2.5em"
9913
- },
9914
- title: (0, i18next.t)("actual size"),
9915
- "data-i18n-title": "actual size",
9916
- "aria-label": (0, i18next.t)("actual size"),
9917
- "data-i18n-aria-label": "actual size"
9918
- },
9919
- [
9920
- "span",
9921
- { style: "font-weight:700;" },
9922
- "1:1"
9923
- ],
9924
- { click: (evt) => {
9925
- evt.stopPropagation();
9926
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ZOOM_RESET", { evt }, gobj);
9927
- } }
9928
- ]);
9929
- center_items.push(camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", toolbar_wide));
9959
+ center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
9960
+ center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
9930
9961
  let current_layout = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout") || DEFAULT_LAYOUT;
9931
9962
  let options = Object.keys(LAYOUTS$1).map(function(key) {
9932
9963
  let attrs = {
@@ -9940,29 +9971,25 @@ function make_toolbar$3(gobj) {
9940
9971
  layout_label(key)
9941
9972
  ];
9942
9973
  });
9943
- let right_items = [
9944
- (0, _yuneta_gobj_js.createElement2)([
9945
- "select",
9946
- {
9947
- class: "JSON_GRAPH_LAYOUT select",
9948
- style: {
9949
- height: toolbar_wide,
9950
- "margin-left": "0.5em"
9951
- },
9952
- title: (0, i18next.t)("layout"),
9953
- "data-i18n-title": "layout",
9954
- "aria-label": (0, i18next.t)("layout"),
9955
- "data-i18n-aria-label": "layout"
9974
+ let right_items = [(0, _yuneta_gobj_js.createElement2)([
9975
+ "select",
9976
+ {
9977
+ class: "JSON_GRAPH_LAYOUT select",
9978
+ style: {
9979
+ height: toolbar_wide,
9980
+ "margin-left": "0.5em"
9956
9981
  },
9957
- options,
9958
- { change: (evt) => {
9959
- evt.stopPropagation();
9960
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CHANGE_LAYOUT", { layout: evt.target.value }, gobj);
9961
- } }
9962
- ]),
9963
- graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
9964
- graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false)
9965
- ];
9982
+ title: (0, i18next.t)("layout"),
9983
+ "data-i18n-title": "layout",
9984
+ "aria-label": (0, i18next.t)("layout"),
9985
+ "data-i18n-aria-label": "layout"
9986
+ },
9987
+ options,
9988
+ { change: (evt) => {
9989
+ evt.stopPropagation();
9990
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CHANGE_LAYOUT", { layout: evt.target.value }, gobj);
9991
+ } }
9992
+ ])].concat(yui_graph_fold_items(gobj, toolbar_wide));
9966
9993
  const $toolbar = yui_toolbar({}, [
9967
9994
  [
9968
9995
  "div",
@@ -10011,7 +10038,7 @@ function build_graph$3(gobj) {
10011
10038
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
10012
10039
  });
10013
10040
  graph.on("aftertransform", () => {
10014
- update_zoom_readout$1(gobj);
10041
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
10015
10042
  });
10016
10043
  graph.on(_antv_g6.CanvasEvent.CLICK, (evt) => {});
10017
10044
  if (typeof ResizeObserver !== "undefined") {
@@ -10272,7 +10299,7 @@ function load_json(gobj) {
10272
10299
  });
10273
10300
  graph.render().then(() => {
10274
10301
  if (!preserve_view) graph.fitView();
10275
- update_zoom_readout$1(gobj);
10302
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
10276
10303
  });
10277
10304
  }
10278
10305
  }
@@ -12582,74 +12609,8 @@ function make_toolbar$1(gobj) {
12582
12609
  let left_items = [];
12583
12610
  let center_items = [];
12584
12611
  let right_items = [];
12585
- for (let item of [
12586
- [
12587
- "yi-magnifying-glass-plus",
12588
- "EV_ZOOM_IN",
12589
- false,
12590
- "i"
12591
- ],
12592
- [
12593
- "yi-magnifying-glass",
12594
- "EV_ZOOM_RESET",
12595
- false,
12596
- "i"
12597
- ],
12598
- [
12599
- "yi-magnifying-glass-minus",
12600
- "EV_ZOOM_OUT",
12601
- false,
12602
- "i"
12603
- ],
12604
- [
12605
- "yi-arrows-to-eye",
12606
- "EV_CENTER",
12607
- false,
12608
- "i"
12609
- ],
12610
- [
12611
- "yi-eye",
12612
- "EV_EXPAND_ALL",
12613
- false,
12614
- "i"
12615
- ],
12616
- [
12617
- "yi-eye-slash",
12618
- "EV_COLLAPSE_ALL",
12619
- false,
12620
- "i"
12621
- ],
12622
- [
12623
- "yi-arrows-rotate",
12624
- "EV_REFRESH",
12625
- false,
12626
- "i"
12627
- ]
12628
- ]) {
12629
- let icon_name = item[0];
12630
- let event_name = item[1];
12631
- let disabled = item[2];
12632
- let button = {
12633
- class: `button ${event_name}`,
12634
- style: {
12635
- height: toolbar_wide,
12636
- width: "2.5em"
12637
- }
12638
- };
12639
- if (disabled) button.disabled = true;
12640
- center_items.push([
12641
- "button",
12642
- button,
12643
- ["i", {
12644
- style: "font-size:1.5em; color:inherit;",
12645
- class: icon_name
12646
- }],
12647
- { click: (evt) => {
12648
- evt.stopPropagation();
12649
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, event_name, { evt }, gobj);
12650
- } }
12651
- ]);
12652
- }
12612
+ center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide).concat(yui_graph_fold_items(gobj, toolbar_wide));
12613
+ center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
12653
12614
  let options = [];
12654
12615
  for (let key of Object.keys(LAYOUTS)) {
12655
12616
  let opt_attrs = { value: key };
@@ -12722,6 +12683,9 @@ function build_graph$2(gobj) {
12722
12683
  behaviors: ["drag-canvas", "zoom-canvas"]
12723
12684
  });
12724
12685
  if (priv.theme) graph.setTheme(priv.theme);
12686
+ graph.on("aftertransform", () => {
12687
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
12688
+ });
12725
12689
  graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
12726
12690
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
12727
12691
  });
@@ -9345,6 +9345,160 @@ function yui_toolbar(attrs = {}, items = []) {
9345
9345
  return $toolbar;
9346
9346
  }
9347
9347
  //#endregion
9348
+ //#region src/yui_graph_camera.js
9349
+ /***********************************************************************
9350
+ * yui_graph_camera.js
9351
+ *
9352
+ * The camera and fold controls a G6 graph puts in a DOM toolbar,
9353
+ * built in ONE place.
9354
+ *
9355
+ * This module exists because the same action was drawn three
9356
+ * different ways across sibling views — actual size as a bare
9357
+ * magnifier here and as `1:1` there, fit as `arrows-to-eye` here and
9358
+ * as corner brackets there, fold as chevrons here and as an eye
9359
+ * there. A user sees two of these graphs side by side in the same
9360
+ * console, so a copied-and-drifted toolbar is not a detail.
9361
+ *
9362
+ * The VOCABULARY is not invented here. It is the one
9363
+ * `c_g6_nodes_tree.js` settled on for the treedb graph, which draws
9364
+ * its toolbar as a G6 plugin over an SVG sprite and therefore cannot
9365
+ * share this code — only its decisions:
9366
+ *
9367
+ * zoom in / zoom out magnifiers
9368
+ * zoom level a READOUT, because without it "1:1" is
9369
+ * a jump to a value nobody was told
9370
+ * fit corner brackets (`yi-fit`, the same
9371
+ * path as the sprite's `g6-icon-fit`)
9372
+ * actual size the WRITTEN `1:1` — the action is
9373
+ * `zoomTo(1)`, and actual size is written
9374
+ * in every editor that offers it, never
9375
+ * drawn, because there is no glyph for it
9376
+ *
9377
+ * Fold is the one the treedb graph does not have, so its vocabulary
9378
+ * comes from the other side of the family: the lazy tree viewer's own
9379
+ * toolbar (`c_yui_json.js`) uses chevrons — open pointing down, closed
9380
+ * pointing right — which is also the direction the per-node handles
9381
+ * speak. An eye means show/hide, which is a different idea.
9382
+ *
9383
+ * Copyright (c) 2026, ArtGins.
9384
+ * All Rights Reserved.
9385
+ ***********************************************************************/
9386
+ /************************************************************
9387
+ * One icon button that fires `event_name`.
9388
+ ************************************************************/
9389
+ function camera_button(gobj, icon, event_name, label_key, wide, extra_style) {
9390
+ return [
9391
+ "button",
9392
+ {
9393
+ class: `button ${event_name}`,
9394
+ type: "button",
9395
+ style: {
9396
+ height: wide,
9397
+ width: "2.5em"
9398
+ },
9399
+ title: t(label_key),
9400
+ "data-i18n-title": label_key,
9401
+ "aria-label": t(label_key),
9402
+ "data-i18n-aria-label": label_key
9403
+ },
9404
+ ["i", {
9405
+ style: "font-size:1.5em; color:inherit;" + (extra_style || ""),
9406
+ class: icon
9407
+ }],
9408
+ { click: (evt) => {
9409
+ evt.stopPropagation();
9410
+ gobj_send_event(gobj, event_name, { evt }, gobj);
9411
+ } }
9412
+ ];
9413
+ }
9414
+ /************************************************************
9415
+ * The current zoom as a percentage.
9416
+ ************************************************************/
9417
+ function yui_graph_zoom_text(graph) {
9418
+ if (!graph) return "100%";
9419
+ try {
9420
+ return Math.round(graph.getZoom() * 100) + "%";
9421
+ } catch (e) {
9422
+ return "100%";
9423
+ }
9424
+ }
9425
+ /************************************************************
9426
+ * Repaint the readout of a container that carries one.
9427
+ *
9428
+ * Call it from G6's `aftertransform`, which is the ONE hook
9429
+ * that also covers the WHEEL — a camera change that passes
9430
+ * through no action, so repainting from each zoom action
9431
+ * leaves the number lying after every notch.
9432
+ ************************************************************/
9433
+ function yui_graph_update_zoom($container, graph) {
9434
+ if (!$container) return;
9435
+ let $readout = $container.querySelector(".GRAPH_ZOOM_LEVEL");
9436
+ if (!$readout) return;
9437
+ $readout.textContent = yui_graph_zoom_text(graph);
9438
+ }
9439
+ /************************************************************
9440
+ * The camera cluster: zoom in, zoom out, readout, fit,
9441
+ * actual size. Returns createElement2 specs, in order.
9442
+ *
9443
+ * `wide` is the host toolbar's button height.
9444
+ ************************************************************/
9445
+ function yui_graph_camera_items(gobj, graph, wide) {
9446
+ return [
9447
+ camera_button(gobj, "yi-magnifying-glass-plus", "EV_ZOOM_IN", "zoom in", wide),
9448
+ camera_button(gobj, "yi-magnifying-glass-minus", "EV_ZOOM_OUT", "zoom out", wide),
9449
+ [
9450
+ "span",
9451
+ {
9452
+ class: "GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey",
9453
+ style: "font-size:.85rem; min-width:3.5em; justify-content:center;",
9454
+ title: t("zoom level"),
9455
+ "data-i18n-title": "zoom level"
9456
+ },
9457
+ yui_graph_zoom_text(graph)
9458
+ ],
9459
+ camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", wide),
9460
+ [
9461
+ "button",
9462
+ {
9463
+ class: "button EV_ZOOM_RESET",
9464
+ type: "button",
9465
+ style: {
9466
+ height: wide,
9467
+ width: "2.5em"
9468
+ },
9469
+ title: t("actual size"),
9470
+ "data-i18n-title": "actual size",
9471
+ "aria-label": t("actual size"),
9472
+ "data-i18n-aria-label": "actual size"
9473
+ },
9474
+ [
9475
+ "span",
9476
+ { style: "font-weight:700;" },
9477
+ "1:1"
9478
+ ],
9479
+ { click: (evt) => {
9480
+ evt.stopPropagation();
9481
+ gobj_send_event(gobj, "EV_ZOOM_RESET", { evt }, gobj);
9482
+ } }
9483
+ ]
9484
+ ];
9485
+ }
9486
+ /************************************************************
9487
+ * The fold pair: expand all, collapse all. Chevrons, the
9488
+ * open one rotated down — the same pair the lazy tree
9489
+ * viewer's toolbar uses, and the same direction the
9490
+ * per-node handles speak.
9491
+ ************************************************************/
9492
+ function yui_graph_fold_items(gobj, wide) {
9493
+ return [camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide, " transform: rotate(90deg);"), camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide)];
9494
+ }
9495
+ /************************************************************
9496
+ * A refresh button, for the graphs that offer one.
9497
+ ************************************************************/
9498
+ function yui_graph_refresh_item(gobj, wide) {
9499
+ return camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", wide);
9500
+ }
9501
+ //#endregion
9348
9502
  //#region src/g6_drag_canvas_touch.js
9349
9503
  /***********************************************************************
9350
9504
  * g6_drag_canvas_touch.js
@@ -9551,7 +9705,6 @@ var PRIVATE_DATA$11 = {
9551
9705
  find_timer: null,
9552
9706
  fold_listener: null,
9553
9707
  $layout_select: null,
9554
- $zoom_readout: null,
9555
9708
  $find_input: null,
9556
9709
  $find_result: null,
9557
9710
  $find_count: null
@@ -9749,84 +9902,6 @@ function destroy_ui$6(gobj) {
9749
9902
  }
9750
9903
  }
9751
9904
  /************************************************************
9752
- * One camera button.
9753
- ************************************************************/
9754
- function camera_button(gobj, icon, event_name, label_key, wide) {
9755
- return [
9756
- "button",
9757
- {
9758
- class: `button ${event_name}`,
9759
- type: "button",
9760
- style: {
9761
- height: wide,
9762
- width: "2.5em"
9763
- },
9764
- title: t(label_key),
9765
- "data-i18n-title": label_key,
9766
- "aria-label": t(label_key),
9767
- "data-i18n-aria-label": label_key
9768
- },
9769
- ["i", {
9770
- style: "font-size:1.5em; color:inherit;",
9771
- class: icon
9772
- }],
9773
- { click: (evt) => {
9774
- evt.stopPropagation();
9775
- gobj_send_event(gobj, event_name, { evt }, gobj);
9776
- } }
9777
- ];
9778
- }
9779
- /************************************************************
9780
- * The zoom, as a percentage. What the two magnifiers
9781
- * change: an editor that offers a zoom shows the number.
9782
- ************************************************************/
9783
- function zoom_percent_text$1(gobj) {
9784
- let graph = gobj.priv.graph;
9785
- if (!graph) return "100%";
9786
- try {
9787
- return Math.round(graph.getZoom() * 100) + "%";
9788
- } catch (e) {
9789
- return "100%";
9790
- }
9791
- }
9792
- /************************************************************
9793
- * Repaint the readout after anything that moves the camera.
9794
- ************************************************************/
9795
- function update_zoom_readout$1(gobj) {
9796
- let priv = gobj.priv;
9797
- if (priv.$zoom_readout) priv.$zoom_readout.textContent = zoom_percent_text$1(gobj);
9798
- }
9799
- /************************************************************
9800
- * One fold button. `open` rotates the chevron to the open
9801
- * state, mirroring the per-node toggle of the lazy tree.
9802
- ************************************************************/
9803
- function graph_fold_button(gobj, event_name, label_key, open) {
9804
- let icon_style = "font-size:1.5em; color:inherit;" + (open ? " transform: rotate(90deg);" : "");
9805
- return [
9806
- "button",
9807
- {
9808
- class: `button ${event_name}`,
9809
- type: "button",
9810
- style: {
9811
- height: gobj_read_attr(gobj, "wide"),
9812
- width: "2.5em"
9813
- },
9814
- title: t(label_key),
9815
- "data-i18n-title": label_key,
9816
- "aria-label": t(label_key),
9817
- "data-i18n-aria-label": label_key
9818
- },
9819
- ["i", {
9820
- style: icon_style,
9821
- class: "yi-chevron-right"
9822
- }],
9823
- { click: (evt) => {
9824
- evt.stopPropagation();
9825
- gobj_send_event(gobj, event_name, {}, gobj);
9826
- } }
9827
- ];
9828
- }
9829
- /************************************************************
9830
9905
  * Toolbar
9831
9906
  ************************************************************/
9832
9907
  function make_toolbar$3(gobj) {
@@ -9876,52 +9951,8 @@ function make_toolbar$3(gobj) {
9876
9951
  ]]
9877
9952
  ]];
9878
9953
  let center_items = [];
9879
- for (let item of [[
9880
- "yi-magnifying-glass-plus",
9881
- "EV_ZOOM_IN",
9882
- "zoom in"
9883
- ], [
9884
- "yi-magnifying-glass-minus",
9885
- "EV_ZOOM_OUT",
9886
- "zoom out"
9887
- ]]) center_items.push(camera_button(gobj, item[0], item[1], item[2], toolbar_wide));
9888
- priv.$zoom_readout = createElement2([
9889
- "span",
9890
- {
9891
- class: "JSON_GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey",
9892
- style: "font-size:.85rem; min-width:3.5em; justify-content:center;",
9893
- title: t("zoom level"),
9894
- "data-i18n-title": "zoom level"
9895
- },
9896
- zoom_percent_text$1(gobj)
9897
- ]);
9898
- center_items.push(priv.$zoom_readout);
9899
- center_items.push(camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", toolbar_wide));
9900
- center_items.push([
9901
- "button",
9902
- {
9903
- class: "button EV_ZOOM_RESET",
9904
- type: "button",
9905
- style: {
9906
- height: toolbar_wide,
9907
- width: "2.5em"
9908
- },
9909
- title: t("actual size"),
9910
- "data-i18n-title": "actual size",
9911
- "aria-label": t("actual size"),
9912
- "data-i18n-aria-label": "actual size"
9913
- },
9914
- [
9915
- "span",
9916
- { style: "font-weight:700;" },
9917
- "1:1"
9918
- ],
9919
- { click: (evt) => {
9920
- evt.stopPropagation();
9921
- gobj_send_event(gobj, "EV_ZOOM_RESET", { evt }, gobj);
9922
- } }
9923
- ]);
9924
- center_items.push(camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", toolbar_wide));
9954
+ center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
9955
+ center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
9925
9956
  let current_layout = gobj_read_str_attr(gobj, "layout") || DEFAULT_LAYOUT;
9926
9957
  let options = Object.keys(LAYOUTS$1).map(function(key) {
9927
9958
  let attrs = {
@@ -9935,29 +9966,25 @@ function make_toolbar$3(gobj) {
9935
9966
  layout_label(key)
9936
9967
  ];
9937
9968
  });
9938
- let right_items = [
9939
- createElement2([
9940
- "select",
9941
- {
9942
- class: "JSON_GRAPH_LAYOUT select",
9943
- style: {
9944
- height: toolbar_wide,
9945
- "margin-left": "0.5em"
9946
- },
9947
- title: t("layout"),
9948
- "data-i18n-title": "layout",
9949
- "aria-label": t("layout"),
9950
- "data-i18n-aria-label": "layout"
9969
+ let right_items = [createElement2([
9970
+ "select",
9971
+ {
9972
+ class: "JSON_GRAPH_LAYOUT select",
9973
+ style: {
9974
+ height: toolbar_wide,
9975
+ "margin-left": "0.5em"
9951
9976
  },
9952
- options,
9953
- { change: (evt) => {
9954
- evt.stopPropagation();
9955
- gobj_send_event(gobj, "EV_CHANGE_LAYOUT", { layout: evt.target.value }, gobj);
9956
- } }
9957
- ]),
9958
- graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
9959
- graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false)
9960
- ];
9977
+ title: t("layout"),
9978
+ "data-i18n-title": "layout",
9979
+ "aria-label": t("layout"),
9980
+ "data-i18n-aria-label": "layout"
9981
+ },
9982
+ options,
9983
+ { change: (evt) => {
9984
+ evt.stopPropagation();
9985
+ gobj_send_event(gobj, "EV_CHANGE_LAYOUT", { layout: evt.target.value }, gobj);
9986
+ } }
9987
+ ])].concat(yui_graph_fold_items(gobj, toolbar_wide));
9961
9988
  const $toolbar = yui_toolbar({}, [
9962
9989
  [
9963
9990
  "div",
@@ -10006,7 +10033,7 @@ function build_graph$3(gobj) {
10006
10033
  gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
10007
10034
  });
10008
10035
  graph.on("aftertransform", () => {
10009
- update_zoom_readout$1(gobj);
10036
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
10010
10037
  });
10011
10038
  graph.on(CanvasEvent.CLICK, (evt) => {});
10012
10039
  if (typeof ResizeObserver !== "undefined") {
@@ -10267,7 +10294,7 @@ function load_json(gobj) {
10267
10294
  });
10268
10295
  graph.render().then(() => {
10269
10296
  if (!preserve_view) graph.fitView();
10270
- update_zoom_readout$1(gobj);
10297
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
10271
10298
  });
10272
10299
  }
10273
10300
  }
@@ -12577,74 +12604,8 @@ function make_toolbar$1(gobj) {
12577
12604
  let left_items = [];
12578
12605
  let center_items = [];
12579
12606
  let right_items = [];
12580
- for (let item of [
12581
- [
12582
- "yi-magnifying-glass-plus",
12583
- "EV_ZOOM_IN",
12584
- false,
12585
- "i"
12586
- ],
12587
- [
12588
- "yi-magnifying-glass",
12589
- "EV_ZOOM_RESET",
12590
- false,
12591
- "i"
12592
- ],
12593
- [
12594
- "yi-magnifying-glass-minus",
12595
- "EV_ZOOM_OUT",
12596
- false,
12597
- "i"
12598
- ],
12599
- [
12600
- "yi-arrows-to-eye",
12601
- "EV_CENTER",
12602
- false,
12603
- "i"
12604
- ],
12605
- [
12606
- "yi-eye",
12607
- "EV_EXPAND_ALL",
12608
- false,
12609
- "i"
12610
- ],
12611
- [
12612
- "yi-eye-slash",
12613
- "EV_COLLAPSE_ALL",
12614
- false,
12615
- "i"
12616
- ],
12617
- [
12618
- "yi-arrows-rotate",
12619
- "EV_REFRESH",
12620
- false,
12621
- "i"
12622
- ]
12623
- ]) {
12624
- let icon_name = item[0];
12625
- let event_name = item[1];
12626
- let disabled = item[2];
12627
- let button = {
12628
- class: `button ${event_name}`,
12629
- style: {
12630
- height: toolbar_wide,
12631
- width: "2.5em"
12632
- }
12633
- };
12634
- if (disabled) button.disabled = true;
12635
- center_items.push([
12636
- "button",
12637
- button,
12638
- ["i", {
12639
- style: "font-size:1.5em; color:inherit;",
12640
- class: icon_name
12641
- }],
12642
- { click: (evt) => {
12643
- evt.stopPropagation();
12644
- gobj_send_event(gobj, event_name, { evt }, gobj);
12645
- } }
12646
- ]);
12647
- }
12607
+ center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide).concat(yui_graph_fold_items(gobj, toolbar_wide));
12608
+ center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
12648
12609
  let options = [];
12649
12610
  for (let key of Object.keys(LAYOUTS)) {
12650
12611
  let opt_attrs = { value: key };
@@ -12717,6 +12678,9 @@ function build_graph$2(gobj) {
12717
12678
  behaviors: ["drag-canvas", "zoom-canvas"]
12718
12679
  });
12719
12680
  if (priv.theme) graph.setTheme(priv.theme);
12681
+ graph.on("aftertransform", () => {
12682
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
12683
+ });
12720
12684
  graph.on(NodeEvent.CLICK, (evt) => {
12721
12685
  gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
12722
12686
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.3",
3
+ "version": "7.23.4",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -40,6 +40,12 @@ import {
40
40
  } from "@yuneta/gobj-js";
41
41
 
42
42
  import {yui_toolbar} from "./yui_toolbar.js";
43
+ import {
44
+ yui_graph_camera_items,
45
+ yui_graph_fold_items,
46
+ yui_graph_refresh_item,
47
+ yui_graph_update_zoom,
48
+ } from "./yui_graph_camera.js";
43
49
 
44
50
  import {t} from "i18next";
45
51
 
@@ -535,40 +541,18 @@ function make_toolbar(gobj)
535
541
  let center_items = [];
536
542
  let right_items = [];
537
543
 
538
- let c_icons = [
539
- ["yi-magnifying-glass-plus", "EV_ZOOM_IN", false, 'i'],
540
- ["yi-magnifying-glass", "EV_ZOOM_RESET", false, 'i'],
541
- ["yi-magnifying-glass-minus", "EV_ZOOM_OUT", false, 'i'],
542
- ["yi-arrows-to-eye", "EV_CENTER", false, 'i'],
543
- ["yi-eye", "EV_EXPAND_ALL", false, 'i'],
544
- ["yi-eye-slash", "EV_COLLAPSE_ALL", false, 'i'],
545
- ["yi-arrows-rotate", "EV_REFRESH", false, 'i'],
546
- ];
547
-
548
- for(let item of c_icons) {
549
- let icon_name = item[0];
550
- let event_name = item[1];
551
- let disabled = item[2];
552
-
553
- let button = {
554
- class: `button ${event_name}`,
555
- style: {height: toolbar_wide, width: '2.5em'}
556
- };
557
- if(disabled) {
558
- button.disabled = true;
559
- }
560
- center_items.push(
561
- ['button', button,
562
- ['i', {style: 'font-size:1.5em; color:inherit;', class: icon_name}],
563
- {
564
- click: (evt) => {
565
- evt.stopPropagation();
566
- gobj_send_event(gobj, event_name, {evt: evt}, gobj);
567
- }
568
- }
569
- ]
570
- );
571
- }
544
+ /*
545
+ * The camera and the fold pair come from yui_graph_camera.js.
546
+ *
547
+ * This toolbar used to draw two of these its own way — a bare
548
+ * magnifier for actual size, `arrows-to-eye` for fit — and fold as
549
+ * an eye, which means show/hide and not open/closed. The same
550
+ * console shows this graph and the JSON one side by side, so the
551
+ * drawings have to be the same drawings.
552
+ */
553
+ center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide)
554
+ .concat(yui_graph_fold_items(gobj, toolbar_wide));
555
+ center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
572
556
 
573
557
  /*
574
558
  * Layout selector
@@ -651,6 +635,12 @@ function build_graph(gobj)
651
635
  graph.setTheme(priv.theme);
652
636
  }
653
637
 
638
+ /* The readout follows ANY camera change, the wheel included — a
639
+ * wheel notch passes through no action of ours. */
640
+ graph.on('aftertransform', () => {
641
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
642
+ });
643
+
654
644
  graph.on(NodeEvent.CLICK, (evt) => {
655
645
  gobj_send_event(gobj, "EV_NODE_CLICK", {evt: evt}, gobj);
656
646
  });
@@ -47,6 +47,12 @@ import {
47
47
  } from "@yuneta/gobj-js";
48
48
 
49
49
  import {yui_toolbar} from "./yui_toolbar.js";
50
+ import {
51
+ yui_graph_camera_items,
52
+ yui_graph_fold_items,
53
+ yui_graph_refresh_item,
54
+ yui_graph_update_zoom,
55
+ } from "./yui_graph_camera.js";
50
56
 
51
57
  import {t} from "i18next";
52
58
 
@@ -179,7 +185,6 @@ let PRIVATE_DATA = {
179
185
  find_timer: null, // rate-limits the find box
180
186
  fold_listener: null, // delegated click on the card fold handles
181
187
  $layout_select: null, // the layout picker
182
- $zoom_readout: null, // the zoom percentage
183
188
  $find_input: null,
184
189
  $find_result: null,
185
190
  $find_count: null,
@@ -455,77 +460,6 @@ function destroy_ui(gobj)
455
460
  }
456
461
  }
457
462
 
458
- /************************************************************
459
- * One camera button.
460
- ************************************************************/
461
- function camera_button(gobj, icon, event_name, label_key, wide)
462
- {
463
- return ['button', {class: `button ${event_name}`, type: 'button',
464
- style: {height: wide, width: '2.5em'},
465
- title: t(label_key), 'data-i18n-title': label_key,
466
- 'aria-label': t(label_key), 'data-i18n-aria-label': label_key},
467
- ['i', {style: 'font-size:1.5em; color:inherit;', class: icon}],
468
- {
469
- click: (evt) => {
470
- evt.stopPropagation();
471
- gobj_send_event(gobj, event_name, {evt: evt}, gobj);
472
- }
473
- }
474
- ];
475
- }
476
-
477
- /************************************************************
478
- * The zoom, as a percentage. What the two magnifiers
479
- * change: an editor that offers a zoom shows the number.
480
- ************************************************************/
481
- function zoom_percent_text(gobj)
482
- {
483
- let graph = gobj.priv.graph;
484
- if(!graph) {
485
- return "100%";
486
- }
487
- try {
488
- return Math.round(graph.getZoom() * 100) + "%";
489
- } catch(e) {
490
- return "100%";
491
- }
492
- }
493
-
494
- /************************************************************
495
- * Repaint the readout after anything that moves the camera.
496
- ************************************************************/
497
- function update_zoom_readout(gobj)
498
- {
499
- let priv = gobj.priv;
500
- if(priv.$zoom_readout) {
501
- priv.$zoom_readout.textContent = zoom_percent_text(gobj);
502
- }
503
- }
504
-
505
- /************************************************************
506
- * One fold button. `open` rotates the chevron to the open
507
- * state, mirroring the per-node toggle of the lazy tree.
508
- ************************************************************/
509
- function graph_fold_button(gobj, event_name, label_key, open)
510
- {
511
- let icon_style = 'font-size:1.5em; color:inherit;' +
512
- (open? ' transform: rotate(90deg);': '');
513
-
514
- return ['button', {class: `button ${event_name}`,
515
- type: 'button',
516
- style: {height: gobj_read_attr(gobj, "wide"), width: '2.5em'},
517
- title: t(label_key), 'data-i18n-title': label_key,
518
- 'aria-label': t(label_key), 'data-i18n-aria-label': label_key},
519
- ['i', {style: icon_style, class: 'yi-chevron-right'}],
520
- {
521
- click: (evt) => {
522
- evt.stopPropagation();
523
- gobj_send_event(gobj, event_name, {}, gobj);
524
- }
525
- }
526
- ];
527
- }
528
-
529
463
  /************************************************************
530
464
  * Toolbar
531
465
  ************************************************************/
@@ -593,54 +527,12 @@ function make_toolbar(gobj)
593
527
  let center_items = [];
594
528
 
595
529
  /*
596
- * The camera, in the SAME vocabulary the treedb graph uses
597
- * (`c_g6_nodes_tree.js`): zoom in / zoom out, then what those two
598
- * change — the zoom READOUT, because without it "1:1" is a jump to
599
- * a value nobody was told — then fit, then actual size.
600
- *
601
- * Two of these used to be a different picture for the same action
602
- * in a sibling view, which is the worst thing an icon can be:
603
- * `EV_ZOOM_RESET` was a bare magnifier and `EV_CENTER` was
604
- * `yi-arrows-to-eye`. Now fit is the same bracket glyph as the
605
- * sprite's `g6-icon-fit`, and actual size is WRITTEN — `1:1` is
606
- * written in every editor that offers it, never drawn, because
607
- * there is no glyph for it.
530
+ * The camera and the fold pair come from yui_graph_camera.js: the
531
+ * same drawings the gobj tree uses, because the two graphs sit in
532
+ * the same console and a copied toolbar drifts.
608
533
  */
609
- let c_icons = [
610
- ["yi-magnifying-glass-plus", "EV_ZOOM_IN", "zoom in"],
611
- ["yi-magnifying-glass-minus", "EV_ZOOM_OUT", "zoom out"],
612
- ];
613
-
614
- for(let item of c_icons) {
615
- center_items.push(camera_button(gobj, item[0], item[1], item[2], toolbar_wide));
616
- }
617
-
618
- priv.$zoom_readout = createElement2(
619
- ['span', {class: 'JSON_GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey',
620
- style: 'font-size:.85rem; min-width:3.5em; justify-content:center;',
621
- title: t('zoom level'), 'data-i18n-title': 'zoom level'},
622
- zoom_percent_text(gobj)]
623
- );
624
- center_items.push(priv.$zoom_readout);
625
-
626
- center_items.push(camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", toolbar_wide));
627
-
628
- /* Written, not drawn — see above. */
629
- center_items.push(
630
- ['button', {class: 'button EV_ZOOM_RESET', type: 'button',
631
- style: {height: toolbar_wide, width: '2.5em'},
632
- title: t('actual size'), 'data-i18n-title': 'actual size',
633
- 'aria-label': t('actual size'), 'data-i18n-aria-label': 'actual size'},
634
- ['span', {style: 'font-weight:700;'}, '1:1'],
635
- {
636
- click: (evt) => {
637
- evt.stopPropagation();
638
- gobj_send_event(gobj, "EV_ZOOM_RESET", {evt: evt}, gobj);
639
- }
640
- }]
641
- );
642
-
643
- center_items.push(camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", toolbar_wide));
534
+ center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
535
+ center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
644
536
 
645
537
  /*
646
538
  * Right: fold the tree.
@@ -681,11 +573,8 @@ function make_toolbar(gobj)
681
573
  }]
682
574
  );
683
575
 
684
- let right_items = [
685
- $layout_select,
686
- graph_fold_button(gobj, "EV_EXPAND_ALL", "expand all", true),
687
- graph_fold_button(gobj, "EV_COLLAPSE_ALL", "collapse all", false),
688
- ];
576
+ let right_items = [$layout_select].concat(
577
+ yui_graph_fold_items(gobj, toolbar_wide));
689
578
 
690
579
  const $toolbar = yui_toolbar({}, [
691
580
  ['div', {class: 'yui-horizontal-toolbar-section left'}, left_items],
@@ -748,7 +637,7 @@ function build_graph(gobj)
748
637
  * node from each zoom action would have left the readout lying
749
638
  * after every notch. */
750
639
  graph.on('aftertransform', () => {
751
- update_zoom_readout(gobj);
640
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
752
641
  });
753
642
 
754
643
  graph.on(CanvasEvent.CLICK, (evt) => {
@@ -1145,7 +1034,7 @@ function load_json(gobj)
1145
1034
  if(!preserve_view) {
1146
1035
  graph.fitView();
1147
1036
  }
1148
- update_zoom_readout(gobj);
1037
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
1149
1038
  });
1150
1039
  }
1151
1040
  }
@@ -0,0 +1,150 @@
1
+ /***********************************************************************
2
+ * yui_graph_camera.js
3
+ *
4
+ * The camera and fold controls a G6 graph puts in a DOM toolbar,
5
+ * built in ONE place.
6
+ *
7
+ * This module exists because the same action was drawn three
8
+ * different ways across sibling views — actual size as a bare
9
+ * magnifier here and as `1:1` there, fit as `arrows-to-eye` here and
10
+ * as corner brackets there, fold as chevrons here and as an eye
11
+ * there. A user sees two of these graphs side by side in the same
12
+ * console, so a copied-and-drifted toolbar is not a detail.
13
+ *
14
+ * The VOCABULARY is not invented here. It is the one
15
+ * `c_g6_nodes_tree.js` settled on for the treedb graph, which draws
16
+ * its toolbar as a G6 plugin over an SVG sprite and therefore cannot
17
+ * share this code — only its decisions:
18
+ *
19
+ * zoom in / zoom out magnifiers
20
+ * zoom level a READOUT, because without it "1:1" is
21
+ * a jump to a value nobody was told
22
+ * fit corner brackets (`yi-fit`, the same
23
+ * path as the sprite's `g6-icon-fit`)
24
+ * actual size the WRITTEN `1:1` — the action is
25
+ * `zoomTo(1)`, and actual size is written
26
+ * in every editor that offers it, never
27
+ * drawn, because there is no glyph for it
28
+ *
29
+ * Fold is the one the treedb graph does not have, so its vocabulary
30
+ * comes from the other side of the family: the lazy tree viewer's own
31
+ * toolbar (`c_yui_json.js`) uses chevrons — open pointing down, closed
32
+ * pointing right — which is also the direction the per-node handles
33
+ * speak. An eye means show/hide, which is a different idea.
34
+ *
35
+ * Copyright (c) 2026, ArtGins.
36
+ * All Rights Reserved.
37
+ ***********************************************************************/
38
+ import {createElement2, gobj_send_event} from "@yuneta/gobj-js";
39
+
40
+ import {t} from "i18next";
41
+
42
+
43
+ /************************************************************
44
+ * One icon button that fires `event_name`.
45
+ ************************************************************/
46
+ function camera_button(gobj, icon, event_name, label_key, wide, extra_style)
47
+ {
48
+ return ['button', {class: `button ${event_name}`, type: 'button',
49
+ style: {height: wide, width: '2.5em'},
50
+ title: t(label_key), 'data-i18n-title': label_key,
51
+ 'aria-label': t(label_key), 'data-i18n-aria-label': label_key},
52
+ ['i', {style: 'font-size:1.5em; color:inherit;' + (extra_style || ''),
53
+ class: icon}],
54
+ {
55
+ click: (evt) => {
56
+ evt.stopPropagation();
57
+ gobj_send_event(gobj, event_name, {evt: evt}, gobj);
58
+ }
59
+ }
60
+ ];
61
+ }
62
+
63
+ /************************************************************
64
+ * The current zoom as a percentage.
65
+ ************************************************************/
66
+ export function yui_graph_zoom_text(graph)
67
+ {
68
+ if(!graph) {
69
+ return "100%";
70
+ }
71
+ try {
72
+ return Math.round(graph.getZoom() * 100) + "%";
73
+ } catch(e) {
74
+ return "100%";
75
+ }
76
+ }
77
+
78
+ /************************************************************
79
+ * Repaint the readout of a container that carries one.
80
+ *
81
+ * Call it from G6's `aftertransform`, which is the ONE hook
82
+ * that also covers the WHEEL — a camera change that passes
83
+ * through no action, so repainting from each zoom action
84
+ * leaves the number lying after every notch.
85
+ ************************************************************/
86
+ export function yui_graph_update_zoom($container, graph)
87
+ {
88
+ if(!$container) {
89
+ return;
90
+ }
91
+ let $readout = $container.querySelector('.GRAPH_ZOOM_LEVEL');
92
+ if(!$readout) {
93
+ return; /* a toolbar without a readout: nobody to report to */
94
+ }
95
+ $readout.textContent = yui_graph_zoom_text(graph);
96
+ }
97
+
98
+ /************************************************************
99
+ * The camera cluster: zoom in, zoom out, readout, fit,
100
+ * actual size. Returns createElement2 specs, in order.
101
+ *
102
+ * `wide` is the host toolbar's button height.
103
+ ************************************************************/
104
+ export function yui_graph_camera_items(gobj, graph, wide)
105
+ {
106
+ return [
107
+ camera_button(gobj, "yi-magnifying-glass-plus", "EV_ZOOM_IN", "zoom in", wide),
108
+ camera_button(gobj, "yi-magnifying-glass-minus", "EV_ZOOM_OUT", "zoom out", wide),
109
+ ['span', {class: 'GRAPH_ZOOM_LEVEL is-flex is-align-items-center px-2 has-text-grey',
110
+ style: 'font-size:.85rem; min-width:3.5em; justify-content:center;',
111
+ title: t('zoom level'), 'data-i18n-title': 'zoom level'},
112
+ yui_graph_zoom_text(graph)],
113
+ camera_button(gobj, "yi-fit", "EV_CENTER", "auto fit", wide),
114
+ /* Written, not drawn — see the header. */
115
+ ['button', {class: 'button EV_ZOOM_RESET', type: 'button',
116
+ style: {height: wide, width: '2.5em'},
117
+ title: t('actual size'), 'data-i18n-title': 'actual size',
118
+ 'aria-label': t('actual size'), 'data-i18n-aria-label': 'actual size'},
119
+ ['span', {style: 'font-weight:700;'}, '1:1'],
120
+ {
121
+ click: (evt) => {
122
+ evt.stopPropagation();
123
+ gobj_send_event(gobj, "EV_ZOOM_RESET", {evt: evt}, gobj);
124
+ }
125
+ }]
126
+ ];
127
+ }
128
+
129
+ /************************************************************
130
+ * The fold pair: expand all, collapse all. Chevrons, the
131
+ * open one rotated down — the same pair the lazy tree
132
+ * viewer's toolbar uses, and the same direction the
133
+ * per-node handles speak.
134
+ ************************************************************/
135
+ export function yui_graph_fold_items(gobj, wide)
136
+ {
137
+ return [
138
+ camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide,
139
+ ' transform: rotate(90deg);'),
140
+ camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide),
141
+ ];
142
+ }
143
+
144
+ /************************************************************
145
+ * A refresh button, for the graphs that offer one.
146
+ ************************************************************/
147
+ export function yui_graph_refresh_item(gobj, wide)
148
+ {
149
+ return camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", wide);
150
+ }