@yuneta/gobj-ui 7.14.2 → 7.15.0

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
@@ -668,6 +668,46 @@ Two implementation notes worth keeping:
668
668
  still arrive from a keyboard path or a form that outlived the flag, and an
669
669
  ignored write is exactly the behaviour this whole change exists to stop.
670
670
 
671
+ ### The graph's viewport toolbar
672
+
673
+ `C_G6_NODES_TREE` floats a vertical toolbar over the canvas:
674
+
675
+ | control | what it does |
676
+ |---|---|
677
+ | zoom in / zoom out | one step of scale |
678
+ | **the zoom level** | a readout, not a button — `85%` |
679
+ | ── | |
680
+ | fit | `fitView()`: the whole graph in the viewport |
681
+ | **`1:1`** | `zoomTo(1)`: actual size |
682
+ | ── | |
683
+ | full screen | the container, not the camera |
684
+
685
+ Two of those rows are the answer to a real complaint, and the reasoning
686
+ generalises:
687
+
688
+ - **`1:1` used to be a house**, and the house was the thing people reached for
689
+ when they wanted the graph back. It never gave it to them: the action is
690
+ `zoomTo(1)`, which sets the **scale** and leaves the camera where it was, so
691
+ from a corner of a large graph it answered with the same corner at 100%. A
692
+ house means *the initial extent* in a map and *the starting view* in an
693
+ editor — never a scale — and this one sat directly under `fit`, so the pair
694
+ read as two ways to do one thing. Actual size is **written** in every editor
695
+ that offers it, because there is no glyph anybody recognises for it.
696
+ - **the zoom level is shown** because `1:1` is a jump to a number, and a jump
697
+ to a number is only meaningful next to the number you are on.
698
+
699
+ The **separators are gaps, not lines**: every item already carries a hairline
700
+ against its neighbour, so one more line would not group anything. Full screen
701
+ is behind the second one because it is a window control that happens to live in
702
+ a camera toolbar.
703
+
704
+ Both toolbars (this one and the edit one) **follow the theme**. They used to be
705
+ pinned to a light background in both themes, with the icon colour pinned dark
706
+ so it survived that — two light islands over a dark canvas.
707
+
708
+ **New keys for consumers: `actual size`, `zoom level`** (both tooltips, so a
709
+ host that has not defined them shows the key on hover and nothing else breaks).
710
+
671
711
  ### Finding a node in the graph
672
712
 
673
713
  `C_YUI_TREEDB_GRAPH` carries a find box in the middle of its toolbar. It
@@ -884,7 +884,7 @@ function mt_start$16(gobj) {
884
884
  "C_YUI_SHELL: invalid config — see browser console for details"
885
885
  ]
886
886
  ]));
887
- return;
887
+ return -1;
888
888
  }
889
889
  let shell_cfg = config.shell || {};
890
890
  if (typeof shell_cfg.remember_section_position === "boolean") (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "remember_section_position", shell_cfg.remember_section_position);
@@ -941,7 +941,7 @@ function mt_start$16(gobj) {
941
941
  ***************************************************************/
942
942
  function mt_stop$16(gobj) {
943
943
  let priv = gobj.priv;
944
- if (!priv) return;
944
+ if (!priv) return 0;
945
945
  close_toolbar_dropdown(gobj);
946
946
  if (priv.hash_handler) {
947
947
  window.removeEventListener("hashchange", priv.hash_handler);
@@ -15656,7 +15656,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15656
15656
  data = webix_msg.data;
15657
15657
  } catch (e) {
15658
15658
  (0, _yuneta_gobj_js.log_error)(e);
15659
- return;
15659
+ return -1;
15660
15660
  }
15661
15661
  let __command__ = (0, _yuneta_gobj_js.msg_iev_get_stack)(gobj, kw, "command_stack", true);
15662
15662
  let command = (0, _yuneta_gobj_js.kw_get_str)(gobj, __command__, "command", "", _yuneta_gobj_js.kw_flag_t.KW_REQUIRED);
@@ -20324,7 +20324,7 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
20324
20324
  data = kw.data;
20325
20325
  } catch (e) {
20326
20326
  (0, _yuneta_gobj_js.log_error)(e);
20327
- return;
20327
+ return -1;
20328
20328
  }
20329
20329
  let __command__ = (0, _yuneta_gobj_js.msg_iev_get_stack)(gobj, kw, "command_stack", true);
20330
20330
  let command = (0, _yuneta_gobj_js.kw_get_str)(gobj, __command__, "command", "", _yuneta_gobj_js.kw_flag_t.KW_REQUIRED);
@@ -53702,11 +53702,22 @@ function inject_svg_icons() {
53702
53702
  ***********************************************************************/
53703
53703
  /***************************************************************
53704
53704
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
53705
- * and disabled support.
53705
+ * and disabled support, plus three item kinds G6 does not have.
53706
53706
  *
53707
53707
  * Each item in getItems() may carry:
53708
53708
  * className — CSS class added to the div (e.g. 'EV_SAVE_GRAPH')
53709
53709
  * disabled — boolean; sets the 'disabled' attribute initially
53710
+ * text — render this text instead of a sprite symbol; a
53711
+ * label is the only readable way to say `1:1`, and
53712
+ * every editor that offers actual-size writes it
53713
+ * readout — a text item that REPORTS instead of acting (the
53714
+ * zoom level); it is not a button
53715
+ * separator — a group divider, not an item
53716
+ *
53717
+ * The base class fires onClick only for elements whose class list
53718
+ * contains `g6-toolbar-item`, so a separator and a readout carry
53719
+ * their own class and are inert by construction, with no guard in
53720
+ * the click handler.
53710
53721
  *
53711
53722
  * With className set to the event name, the lib_graph.js state
53712
53723
  * functions (set_submit_state, disableElements, …) work on toolbar
@@ -53716,9 +53727,15 @@ function inject_svg_icons() {
53716
53727
  var YuiToolbar = class extends _antv_g6.Toolbar {
53717
53728
  async getDOMContent() {
53718
53729
  return (await this.options.getItems()).map((item) => {
53730
+ if (item.separator) return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
53719
53731
  const extra_class = item.className ? ` ${item.className}` : "";
53732
+ const title = item.title ?? "";
53733
+ if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}">${item.text ?? ""}</div>`;
53720
53734
  const disabled = item.disabled ? " disabled" : "";
53721
- return `<div class="g6-toolbar-item${extra_class}" value="${item.value}" title="${item.title ?? ""}"${disabled}><svg aria-hidden="true" focusable="false"><use xlink:href="#${item.id}"></use></svg></div>`;
53735
+ const is_text = item.text !== void 0;
53736
+ const kind_class = is_text ? " g6-toolbar-item-text" : "";
53737
+ const body = is_text ? `<span class="g6-toolbar-text">${item.text}</span>` : `<svg aria-hidden="true" focusable="false"><use xlink:href="#${item.id}"></use></svg>`;
53738
+ return `<div class="g6-toolbar-item${kind_class}${extra_class}" value="${item.value}" title="${title}" aria-label="${title}"${disabled}>${body}</div>`;
53722
53739
  }).join("");
53723
53740
  }
53724
53741
  };
@@ -54063,6 +54080,7 @@ function configure_events(gobj) {
54063
54080
  update_edge_icon_position(gobj);
54064
54081
  update_node_icon_position(gobj);
54065
54082
  update_link_icon_position(gobj);
54083
+ update_zoom_readout(gobj);
54066
54084
  });
54067
54085
  priv._on_language_changed = () => {
54068
54086
  update_toolbar(gobj);
@@ -54128,6 +54146,59 @@ function update_toolbar(gobj) {
54128
54146
  configure_toolbar_edit(gobj);
54129
54147
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
54130
54148
  }
54149
+ /************************************************************
54150
+ * The floating toolbars follow the theme.
54151
+ *
54152
+ * They used to be pinned to a light background in BOTH themes,
54153
+ * with the icon colour pinned dark so it survived that -- two
54154
+ * light islands sitting over a dark canvas. The tokens do the
54155
+ * flipping now; the fallbacks are the old forced-light values, so
54156
+ * a host without Bulma gets exactly what it had.
54157
+ ************************************************************/
54158
+ function toolbar_style(extra) {
54159
+ return Object.assign({
54160
+ backgroundColor: "var(--bulma-scheme-main, #f5f5f5)",
54161
+ color: "var(--bulma-text-strong, #333)",
54162
+ padding: "8px",
54163
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54164
+ borderRadius: "8px",
54165
+ border: "1px solid var(--bulma-border-weak, #e8e8e8)",
54166
+ opacity: "0.85"
54167
+ }, extra || {});
54168
+ }
54169
+ /************************************************************
54170
+ * The zoom level as a percentage, the way every editor that
54171
+ * shows one writes it. Empty while the graph cannot be asked --
54172
+ * the toolbar is built before the first draw, and a viewport
54173
+ * that does not exist yet has no zoom to report.
54174
+ ************************************************************/
54175
+ function zoom_percent_text(gobj) {
54176
+ let graph = gobj.priv.graph;
54177
+ if (!graph || typeof graph.getZoom !== "function") return "";
54178
+ let zoom;
54179
+ try {
54180
+ zoom = graph.getZoom();
54181
+ } catch (e) {
54182
+ return "";
54183
+ }
54184
+ if (!zoom) return "";
54185
+ return `${Math.round(zoom * 100)}%`;
54186
+ }
54187
+ /************************************************************
54188
+ * Repaint the zoom readout in place.
54189
+ *
54190
+ * The text node is patched instead of re-rendering the plugin:
54191
+ * the readout follows the wheel, and updatePlugin() rebuilds the
54192
+ * whole toolbar's innerHTML -- which would also drop the disabled
54193
+ * state of the edit buttons on every notch of the wheel.
54194
+ ************************************************************/
54195
+ function update_zoom_readout(gobj) {
54196
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
54197
+ if (!$container) return;
54198
+ let $readout = $container.querySelector(".G6_ZOOM_LEVEL");
54199
+ if (!$readout) return;
54200
+ $readout.textContent = zoom_percent_text(gobj);
54201
+ }
54131
54202
  function configure_toolbar(gobj) {
54132
54203
  let priv = gobj.priv;
54133
54204
  let toolbar_position = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "toolbar_position") || "top-left";
@@ -54136,17 +54207,10 @@ function configure_toolbar(gobj) {
54136
54207
  type: "yui-toolbar",
54137
54208
  className: "g6-toolbar-large",
54138
54209
  position: toolbar_position,
54139
- style: {
54140
- backgroundColor: "#f5f5f5",
54141
- color: "#333",
54142
- padding: "8px",
54143
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54144
- borderRadius: "8px",
54145
- border: "1px solid #e8e8e8",
54146
- opacity: "0.85",
54210
+ style: toolbar_style({
54147
54211
  marginTop: "12px",
54148
54212
  marginLeft: "12px"
54149
- },
54213
+ }),
54150
54214
  getItems: () => {
54151
54215
  let items = [
54152
54216
  {
@@ -54161,6 +54225,13 @@ function configure_toolbar(gobj) {
54161
54225
  className: "EV_ZOOM_OUT",
54162
54226
  title: (0, i18next.t)("zoom out")
54163
54227
  },
54228
+ {
54229
+ readout: true,
54230
+ className: "G6_ZOOM_LEVEL",
54231
+ text: zoom_percent_text(gobj),
54232
+ title: (0, i18next.t)("zoom level")
54233
+ },
54234
+ { separator: true },
54164
54235
  {
54165
54236
  id: "g6-icon-fit",
54166
54237
  value: "auto-fit",
@@ -54168,13 +54239,14 @@ function configure_toolbar(gobj) {
54168
54239
  title: (0, i18next.t)("auto fit")
54169
54240
  },
54170
54241
  {
54171
- id: "g6-icon-home",
54242
+ text: "1:1",
54172
54243
  value: "reset",
54173
54244
  className: "EV_ZOOM_RESET",
54174
- title: (0, i18next.t)("reset zoom")
54245
+ title: (0, i18next.t)("actual size")
54175
54246
  }
54176
54247
  ];
54177
54248
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
54249
+ items.push({ separator: true });
54178
54250
  if (priv.is_fullscreen) items.push({
54179
54251
  id: "g6-icon-fullscreen-exit",
54180
54252
  value: "exit-fullscreen",
@@ -54230,15 +54302,7 @@ function configure_toolbar_edit(gobj) {
54230
54302
  type: "yui-toolbar",
54231
54303
  className: "g6-toolbar-large",
54232
54304
  position: "left-top",
54233
- style: {
54234
- backgroundColor: "#f5f5f5",
54235
- color: "#333",
54236
- padding: "8px",
54237
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54238
- borderRadius: "8px",
54239
- border: "1px solid #e8e8e8",
54240
- opacity: "0.85"
54241
- },
54305
+ style: toolbar_style(),
54242
54306
  getItems: () => {
54243
54307
  return [
54244
54308
  {
@@ -879,7 +879,7 @@ function mt_start$16(gobj) {
879
879
  "C_YUI_SHELL: invalid config — see browser console for details"
880
880
  ]
881
881
  ]));
882
- return;
882
+ return -1;
883
883
  }
884
884
  let shell_cfg = config.shell || {};
885
885
  if (typeof shell_cfg.remember_section_position === "boolean") gobj_write_attr(gobj, "remember_section_position", shell_cfg.remember_section_position);
@@ -936,7 +936,7 @@ function mt_start$16(gobj) {
936
936
  ***************************************************************/
937
937
  function mt_stop$16(gobj) {
938
938
  let priv = gobj.priv;
939
- if (!priv) return;
939
+ if (!priv) return 0;
940
940
  close_toolbar_dropdown(gobj);
941
941
  if (priv.hash_handler) {
942
942
  window.removeEventListener("hashchange", priv.hash_handler);
@@ -15651,7 +15651,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15651
15651
  data = webix_msg.data;
15652
15652
  } catch (e) {
15653
15653
  log_error(e);
15654
- return;
15654
+ return -1;
15655
15655
  }
15656
15656
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
15657
15657
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
@@ -20311,7 +20311,7 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
20311
20311
  data = kw.data;
20312
20312
  } catch (e) {
20313
20313
  log_error(e);
20314
- return;
20314
+ return -1;
20315
20315
  }
20316
20316
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
20317
20317
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
@@ -53689,11 +53689,22 @@ function inject_svg_icons() {
53689
53689
  ***********************************************************************/
53690
53690
  /***************************************************************
53691
53691
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
53692
- * and disabled support.
53692
+ * and disabled support, plus three item kinds G6 does not have.
53693
53693
  *
53694
53694
  * Each item in getItems() may carry:
53695
53695
  * className — CSS class added to the div (e.g. 'EV_SAVE_GRAPH')
53696
53696
  * disabled — boolean; sets the 'disabled' attribute initially
53697
+ * text — render this text instead of a sprite symbol; a
53698
+ * label is the only readable way to say `1:1`, and
53699
+ * every editor that offers actual-size writes it
53700
+ * readout — a text item that REPORTS instead of acting (the
53701
+ * zoom level); it is not a button
53702
+ * separator — a group divider, not an item
53703
+ *
53704
+ * The base class fires onClick only for elements whose class list
53705
+ * contains `g6-toolbar-item`, so a separator and a readout carry
53706
+ * their own class and are inert by construction, with no guard in
53707
+ * the click handler.
53697
53708
  *
53698
53709
  * With className set to the event name, the lib_graph.js state
53699
53710
  * functions (set_submit_state, disableElements, …) work on toolbar
@@ -53703,9 +53714,15 @@ function inject_svg_icons() {
53703
53714
  var YuiToolbar = class extends Toolbar {
53704
53715
  async getDOMContent() {
53705
53716
  return (await this.options.getItems()).map((item) => {
53717
+ if (item.separator) return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
53706
53718
  const extra_class = item.className ? ` ${item.className}` : "";
53719
+ const title = item.title ?? "";
53720
+ if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}">${item.text ?? ""}</div>`;
53707
53721
  const disabled = item.disabled ? " disabled" : "";
53708
- return `<div class="g6-toolbar-item${extra_class}" value="${item.value}" title="${item.title ?? ""}"${disabled}><svg aria-hidden="true" focusable="false"><use xlink:href="#${item.id}"></use></svg></div>`;
53722
+ const is_text = item.text !== void 0;
53723
+ const kind_class = is_text ? " g6-toolbar-item-text" : "";
53724
+ const body = is_text ? `<span class="g6-toolbar-text">${item.text}</span>` : `<svg aria-hidden="true" focusable="false"><use xlink:href="#${item.id}"></use></svg>`;
53725
+ return `<div class="g6-toolbar-item${kind_class}${extra_class}" value="${item.value}" title="${title}" aria-label="${title}"${disabled}>${body}</div>`;
53709
53726
  }).join("");
53710
53727
  }
53711
53728
  };
@@ -54050,6 +54067,7 @@ function configure_events(gobj) {
54050
54067
  update_edge_icon_position(gobj);
54051
54068
  update_node_icon_position(gobj);
54052
54069
  update_link_icon_position(gobj);
54070
+ update_zoom_readout(gobj);
54053
54071
  });
54054
54072
  priv._on_language_changed = () => {
54055
54073
  update_toolbar(gobj);
@@ -54115,6 +54133,59 @@ function update_toolbar(gobj) {
54115
54133
  configure_toolbar_edit(gobj);
54116
54134
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
54117
54135
  }
54136
+ /************************************************************
54137
+ * The floating toolbars follow the theme.
54138
+ *
54139
+ * They used to be pinned to a light background in BOTH themes,
54140
+ * with the icon colour pinned dark so it survived that -- two
54141
+ * light islands sitting over a dark canvas. The tokens do the
54142
+ * flipping now; the fallbacks are the old forced-light values, so
54143
+ * a host without Bulma gets exactly what it had.
54144
+ ************************************************************/
54145
+ function toolbar_style(extra) {
54146
+ return Object.assign({
54147
+ backgroundColor: "var(--bulma-scheme-main, #f5f5f5)",
54148
+ color: "var(--bulma-text-strong, #333)",
54149
+ padding: "8px",
54150
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54151
+ borderRadius: "8px",
54152
+ border: "1px solid var(--bulma-border-weak, #e8e8e8)",
54153
+ opacity: "0.85"
54154
+ }, extra || {});
54155
+ }
54156
+ /************************************************************
54157
+ * The zoom level as a percentage, the way every editor that
54158
+ * shows one writes it. Empty while the graph cannot be asked --
54159
+ * the toolbar is built before the first draw, and a viewport
54160
+ * that does not exist yet has no zoom to report.
54161
+ ************************************************************/
54162
+ function zoom_percent_text(gobj) {
54163
+ let graph = gobj.priv.graph;
54164
+ if (!graph || typeof graph.getZoom !== "function") return "";
54165
+ let zoom;
54166
+ try {
54167
+ zoom = graph.getZoom();
54168
+ } catch (e) {
54169
+ return "";
54170
+ }
54171
+ if (!zoom) return "";
54172
+ return `${Math.round(zoom * 100)}%`;
54173
+ }
54174
+ /************************************************************
54175
+ * Repaint the zoom readout in place.
54176
+ *
54177
+ * The text node is patched instead of re-rendering the plugin:
54178
+ * the readout follows the wheel, and updatePlugin() rebuilds the
54179
+ * whole toolbar's innerHTML -- which would also drop the disabled
54180
+ * state of the edit buttons on every notch of the wheel.
54181
+ ************************************************************/
54182
+ function update_zoom_readout(gobj) {
54183
+ let $container = gobj_read_attr(gobj, "$container");
54184
+ if (!$container) return;
54185
+ let $readout = $container.querySelector(".G6_ZOOM_LEVEL");
54186
+ if (!$readout) return;
54187
+ $readout.textContent = zoom_percent_text(gobj);
54188
+ }
54118
54189
  function configure_toolbar(gobj) {
54119
54190
  let priv = gobj.priv;
54120
54191
  let toolbar_position = gobj_read_str_attr(gobj, "toolbar_position") || "top-left";
@@ -54123,17 +54194,10 @@ function configure_toolbar(gobj) {
54123
54194
  type: "yui-toolbar",
54124
54195
  className: "g6-toolbar-large",
54125
54196
  position: toolbar_position,
54126
- style: {
54127
- backgroundColor: "#f5f5f5",
54128
- color: "#333",
54129
- padding: "8px",
54130
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54131
- borderRadius: "8px",
54132
- border: "1px solid #e8e8e8",
54133
- opacity: "0.85",
54197
+ style: toolbar_style({
54134
54198
  marginTop: "12px",
54135
54199
  marginLeft: "12px"
54136
- },
54200
+ }),
54137
54201
  getItems: () => {
54138
54202
  let items = [
54139
54203
  {
@@ -54148,6 +54212,13 @@ function configure_toolbar(gobj) {
54148
54212
  className: "EV_ZOOM_OUT",
54149
54213
  title: t("zoom out")
54150
54214
  },
54215
+ {
54216
+ readout: true,
54217
+ className: "G6_ZOOM_LEVEL",
54218
+ text: zoom_percent_text(gobj),
54219
+ title: t("zoom level")
54220
+ },
54221
+ { separator: true },
54151
54222
  {
54152
54223
  id: "g6-icon-fit",
54153
54224
  value: "auto-fit",
@@ -54155,13 +54226,14 @@ function configure_toolbar(gobj) {
54155
54226
  title: t("auto fit")
54156
54227
  },
54157
54228
  {
54158
- id: "g6-icon-home",
54229
+ text: "1:1",
54159
54230
  value: "reset",
54160
54231
  className: "EV_ZOOM_RESET",
54161
- title: t("reset zoom")
54232
+ title: t("actual size")
54162
54233
  }
54163
54234
  ];
54164
54235
  if (gobj_read_bool_attr(gobj, "with_fullscreen")) {
54236
+ items.push({ separator: true });
54165
54237
  if (priv.is_fullscreen) items.push({
54166
54238
  id: "g6-icon-fullscreen-exit",
54167
54239
  value: "exit-fullscreen",
@@ -54217,15 +54289,7 @@ function configure_toolbar_edit(gobj) {
54217
54289
  type: "yui-toolbar",
54218
54290
  className: "g6-toolbar-large",
54219
54291
  position: "left-top",
54220
- style: {
54221
- backgroundColor: "#f5f5f5",
54222
- color: "#333",
54223
- padding: "8px",
54224
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54225
- borderRadius: "8px",
54226
- border: "1px solid #e8e8e8",
54227
- opacity: "0.85"
54228
- },
54292
+ style: toolbar_style(),
54229
54293
  getItems: () => {
54230
54294
  return [
54231
54295
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.14.2",
3
+ "version": "7.15.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -105,11 +105,22 @@ import {yui_theme_now, yui_watch_theme} from "./yui_theme.js";
105
105
 
106
106
  /***************************************************************
107
107
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
108
- * and disabled support.
108
+ * and disabled support, plus three item kinds G6 does not have.
109
109
  *
110
110
  * Each item in getItems() may carry:
111
111
  * className — CSS class added to the div (e.g. 'EV_SAVE_GRAPH')
112
112
  * disabled — boolean; sets the 'disabled' attribute initially
113
+ * text — render this text instead of a sprite symbol; a
114
+ * label is the only readable way to say `1:1`, and
115
+ * every editor that offers actual-size writes it
116
+ * readout — a text item that REPORTS instead of acting (the
117
+ * zoom level); it is not a button
118
+ * separator — a group divider, not an item
119
+ *
120
+ * The base class fires onClick only for elements whose class list
121
+ * contains `g6-toolbar-item`, so a separator and a readout carry
122
+ * their own class and are inert by construction, with no guard in
123
+ * the click handler.
113
124
  *
114
125
  * With className set to the event name, the lib_graph.js state
115
126
  * functions (set_submit_state, disableElements, …) work on toolbar
@@ -122,14 +133,36 @@ class YuiToolbar extends Toolbar
122
133
  {
123
134
  const items = await this.options.getItems();
124
135
  return items.map((item) => {
136
+ if(item.separator) {
137
+ return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
138
+ }
139
+
125
140
  const extra_class = item.className ? ` ${item.className}` : '';
126
- const disabled = item.disabled ? ' disabled' : '';
141
+ const title = item.title ?? '';
142
+
143
+ if(item.readout) {
144
+ return (
145
+ `<div class="g6-toolbar-readout${extra_class}"` +
146
+ ` title="${title}" aria-label="${title}">` +
147
+ `${item.text ?? ''}` +
148
+ `</div>`
149
+ );
150
+ }
151
+
152
+ const disabled = item.disabled ? ' disabled' : '';
153
+ const is_text = (item.text !== undefined);
154
+ const kind_class = is_text ? ' g6-toolbar-item-text' : '';
155
+ const body = is_text
156
+ ? `<span class="g6-toolbar-text">${item.text}</span>`
157
+ : `<svg aria-hidden="true" focusable="false">` +
158
+ `<use xlink:href="#${item.id}"></use>` +
159
+ `</svg>`;
160
+
127
161
  return (
128
- `<div class="g6-toolbar-item${extra_class}"` +
129
- ` value="${item.value}" title="${item.title ?? ''}"${disabled}>` +
130
- `<svg aria-hidden="true" focusable="false">` +
131
- `<use xlink:href="#${item.id}"></use>` +
132
- `</svg>` +
162
+ `<div class="g6-toolbar-item${kind_class}${extra_class}"` +
163
+ ` value="${item.value}" title="${title}"` +
164
+ ` aria-label="${title}"${disabled}>` +
165
+ `${body}` +
133
166
  `</div>`
134
167
  );
135
168
  }).join('');
@@ -732,6 +765,7 @@ function configure_events(gobj)
732
765
  update_edge_icon_position(gobj);
733
766
  update_node_icon_position(gobj);
734
767
  update_link_icon_position(gobj);
768
+ update_zoom_readout(gobj);
735
769
  });
736
770
 
737
771
  // Re-render G6 toolbars when language changes
@@ -863,6 +897,79 @@ function update_toolbar(gobj)
863
897
  }
864
898
  }
865
899
 
900
+ /************************************************************
901
+ * The floating toolbars follow the theme.
902
+ *
903
+ * They used to be pinned to a light background in BOTH themes,
904
+ * with the icon colour pinned dark so it survived that -- two
905
+ * light islands sitting over a dark canvas. The tokens do the
906
+ * flipping now; the fallbacks are the old forced-light values, so
907
+ * a host without Bulma gets exactly what it had.
908
+ ************************************************************/
909
+ function toolbar_style(extra)
910
+ {
911
+ return Object.assign({
912
+ backgroundColor: 'var(--bulma-scheme-main, #f5f5f5)',
913
+ color: 'var(--bulma-text-strong, #333)',
914
+ padding: '8px',
915
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
916
+ borderRadius: '8px',
917
+ border: '1px solid var(--bulma-border-weak, #e8e8e8)',
918
+ opacity: '0.85',
919
+ }, extra || {});
920
+ }
921
+
922
+ /************************************************************
923
+ * The zoom level as a percentage, the way every editor that
924
+ * shows one writes it. Empty while the graph cannot be asked --
925
+ * the toolbar is built before the first draw, and a viewport
926
+ * that does not exist yet has no zoom to report.
927
+ ************************************************************/
928
+ function zoom_percent_text(gobj)
929
+ {
930
+ let graph = gobj.priv.graph;
931
+
932
+ if(!graph || typeof graph.getZoom !== "function") {
933
+ return "";
934
+ }
935
+
936
+ let zoom;
937
+ try {
938
+ zoom = graph.getZoom();
939
+ } catch(e) {
940
+ return "";
941
+ }
942
+ if(!zoom) {
943
+ return "";
944
+ }
945
+
946
+ return `${Math.round(zoom * 100)}%`;
947
+ }
948
+
949
+ /************************************************************
950
+ * Repaint the zoom readout in place.
951
+ *
952
+ * The text node is patched instead of re-rendering the plugin:
953
+ * the readout follows the wheel, and updatePlugin() rebuilds the
954
+ * whole toolbar's innerHTML -- which would also drop the disabled
955
+ * state of the edit buttons on every notch of the wheel.
956
+ ************************************************************/
957
+ function update_zoom_readout(gobj)
958
+ {
959
+ let $container = gobj_read_attr(gobj, "$container");
960
+
961
+ if(!$container) {
962
+ return;
963
+ }
964
+
965
+ let $readout = $container.querySelector(".G6_ZOOM_LEVEL");
966
+ if(!$readout) {
967
+ return; /* no toolbar (with_toolbar false): nobody to report to */
968
+ }
969
+
970
+ $readout.textContent = zoom_percent_text(gobj);
971
+ }
972
+
866
973
  function configure_toolbar(gobj)
867
974
  {
868
975
  let priv = gobj.priv;
@@ -880,28 +987,37 @@ function configure_toolbar(gobj)
880
987
  type: 'yui-toolbar',
881
988
  className: 'g6-toolbar-large',
882
989
  position: toolbar_position,
883
- style: {
884
- backgroundColor: '#f5f5f5',
885
- /* Forced-light bg in both themes → pin dark icon
886
- * color (currentColor) so it stays visible in dark. */
887
- color: '#333',
888
- padding: '8px',
889
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
890
- borderRadius: '8px',
891
- border: '1px solid #e8e8e8',
892
- opacity: '0.85',
893
- marginTop: '12px',
990
+ style: toolbar_style({
991
+ marginTop: '12px',
894
992
  marginLeft: '12px',
895
- },
993
+ }),
896
994
  getItems: () => {
897
995
  let items = [
898
996
  { id: 'g6-icon-zoom-in', value: 'zoom-in', className: 'EV_ZOOM_IN', title: t('zoom in') },
899
997
  { id: 'g6-icon-zoom-out', value: 'zoom-out', className: 'EV_ZOOM_OUT', title: t('zoom out') },
998
+ /* What the two buttons above change. Every editor
999
+ * that offers a zoom shows the number; without it
1000
+ * `1:1` is a jump to a value nobody was told. */
1001
+ { readout: true, className: 'G6_ZOOM_LEVEL',
1002
+ text: zoom_percent_text(gobj), title: t('zoom level') },
1003
+ { separator: true },
900
1004
  { id: 'g6-icon-fit', value: 'auto-fit', className: 'EV_AUTO_FIT', title: t('auto fit') },
901
- { id: 'g6-icon-home', value: 'reset', className: 'EV_ZOOM_RESET', title: t('reset zoom') },
1005
+ /* `1:1`, not a house. The action is `zoomTo(1)`: it
1006
+ * sets the SCALE and leaves the camera where it was,
1007
+ * which is neither of the two things a house means
1008
+ * anywhere -- a map's initial extent, an editor's
1009
+ * starting view. Sitting under `fit`, the house read
1010
+ * as a second way to get the whole graph back, and
1011
+ * answered with the same corner of it at 100%.
1012
+ * Actual size is WRITTEN in every editor that offers
1013
+ * it, never drawn: there is no glyph for it. */
1014
+ { text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size') },
902
1015
  ];
903
1016
 
904
1017
  if(gobj_read_bool_attr(gobj, "with_fullscreen")) {
1018
+ /* Full screen is a WINDOW control, not a camera one:
1019
+ * it goes in its own group. */
1020
+ items.push({ separator: true });
905
1021
  if(priv.is_fullscreen) {
906
1022
  items.push(
907
1023
  { id: 'g6-icon-fullscreen-exit', value: 'exit-fullscreen', className: 'EV_EXIT_FULLSCREEN', title: t('exit full screen') }
@@ -976,18 +1092,7 @@ function configure_toolbar_edit(gobj)
976
1092
  type: 'yui-toolbar',
977
1093
  className: 'g6-toolbar-large',
978
1094
  position: 'left-top',
979
- style: {
980
- backgroundColor: '#f5f5f5',
981
- /* Toolbar bg is forced light in BOTH themes; icons
982
- * use currentColor, so pin a dark color or in dark
983
- * theme they'd be light-on-light (invisible). */
984
- color: '#333',
985
- padding: '8px',
986
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
987
- borderRadius: '8px',
988
- border: '1px solid #e8e8e8',
989
- opacity: '0.85',
990
- },
1095
+ style: toolbar_style(),
991
1096
  getItems: () => {
992
1097
  return [
993
1098
  { id: 'g6-icon-plus', value: 'create-node', className: 'EV_CREATE_NODE_BTN color_create_state', title: t('create node') },
package/src/c_yui_node.js CHANGED
@@ -275,7 +275,7 @@ function mt_start(gobj)
275
275
  }
276
276
 
277
277
  if(!priv.is_root) {
278
- return;
278
+ return 0;
279
279
  }
280
280
 
281
281
  /*
@@ -287,7 +287,7 @@ function mt_start(gobj)
287
287
  let shell = yui_shell_of(gobj);
288
288
  if(!shell) {
289
289
  log_error(`${GCLASS_NAME}: no shell — the tree cannot route`);
290
- return;
290
+ return -1;
291
291
  }
292
292
  gobj_subscribe_event(shell, "EV_ROUTE_CHANGED", {}, gobj);
293
293
  publish_sub_routes(gobj);
@@ -226,7 +226,7 @@ function mt_start(gobj)
226
226
  ]
227
227
  ));
228
228
  }
229
- return;
229
+ return -1;
230
230
  }
231
231
 
232
232
  /* Declarative sugar over the attr, which stays the runtime truth:
@@ -364,7 +364,7 @@ function mt_stop(gobj)
364
364
  {
365
365
  let priv = gobj.priv;
366
366
  if(!priv) {
367
- return;
367
+ return 0;
368
368
  }
369
369
 
370
370
  /* Tear down any toolbar dropdown that was open at stop time so its
@@ -1495,7 +1495,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
1495
1495
  data = kw.data;
1496
1496
  } catch (e) {
1497
1497
  log_error(e);
1498
- return;
1498
+ return -1;
1499
1499
  }
1500
1500
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
1501
1501
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
@@ -1721,7 +1721,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
1721
1721
  data = webix_msg.data;
1722
1722
  } catch (e) {
1723
1723
  log_error(e);
1724
- return;
1724
+ return -1;
1725
1725
  }
1726
1726
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
1727
1727
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
package/src/lib_graph.css CHANGED
@@ -55,13 +55,79 @@
55
55
  opacity: 0.35;
56
56
  pointer-events: none;
57
57
  }
58
- /* Vertical toolbar (column): horizontal separator */
58
+ /* Vertical toolbar (column): horizontal hairline between items */
59
59
  .g6-toolbar.g6-toolbar-large[style*="flex-direction: column"] .g6-toolbar-item + .g6-toolbar-item {
60
- border-top: 1px solid #d9d9d9;
60
+ border-top: 1px solid var(--bulma-border-weak, #d9d9d9);
61
61
  }
62
- /* Horizontal toolbar (row): vertical separator */
62
+ /* Horizontal toolbar (row): vertical hairline between items */
63
63
  .g6-toolbar.g6-toolbar-large[style*="flex-direction: row"] .g6-toolbar-item + .g6-toolbar-item {
64
- border-left: 1px solid #d9d9d9;
64
+ border-left: 1px solid var(--bulma-border-weak, #d9d9d9);
65
+ }
66
+
67
+ /*
68
+ * Group divider. Every item already carries a hairline against its
69
+ * neighbour, so a group break has to be a GAP, not one more line --
70
+ * otherwise the camera controls and the window control read as one
71
+ * list of six equal things.
72
+ */
73
+ .g6-toolbar.g6-toolbar-large .g6-toolbar-sep {
74
+ background-color: var(--bulma-border, #cfcfcf);
75
+ flex: none;
76
+ }
77
+ .g6-toolbar.g6-toolbar-large[style*="flex-direction: column"] .g6-toolbar-sep {
78
+ width: 100%;
79
+ height: 1px;
80
+ margin: 5px 0;
81
+ }
82
+ .g6-toolbar.g6-toolbar-large[style*="flex-direction: row"] .g6-toolbar-sep {
83
+ width: 1px;
84
+ align-self: stretch;
85
+ margin: 0 5px;
86
+ }
87
+
88
+ /*
89
+ * A toolbar item whose glyph is TEXT (`1:1`). There is no icon for
90
+ * actual size, so the item has to be as wide as its label instead
91
+ * of the square an icon gets.
92
+ */
93
+ .g6-toolbar.g6-toolbar-large .g6-toolbar-item-text {
94
+ width: auto;
95
+ min-width: 20px;
96
+ }
97
+ .g6-toolbar.g6-toolbar-large .g6-toolbar-text {
98
+ display: block;
99
+ line-height: 20px;
100
+ font-size: 0.8125rem;
101
+ font-weight: 600;
102
+ text-align: center;
103
+ white-space: nowrap;
104
+ font-variant-numeric: tabular-nums;
105
+ }
106
+
107
+ /*
108
+ * The zoom readout REPORTS, it does not act: no pointer, no hover,
109
+ * and quieter than the buttons it sits with. It carries no
110
+ * `g6-toolbar-item` class, which is also what keeps G6's own click
111
+ * handler from firing on it.
112
+ */
113
+ .g6-toolbar.g6-toolbar-large .g6-toolbar-readout {
114
+ padding: 2px 6px;
115
+ font-size: 0.75rem;
116
+ text-align: center;
117
+ white-space: nowrap;
118
+ cursor: default;
119
+ opacity: 0.7;
120
+ font-variant-numeric: tabular-nums;
121
+ }
122
+
123
+ /*
124
+ * Hover follows the theme too. G6 injects `.g6-toolbar-item:hover
125
+ * { background-color: #f0f0f0 }` at runtime, AFTER our bundled CSS,
126
+ * so an equal-specificity rule would lose -- the extra class on the
127
+ * toolbar itself is what wins it, with no !important.
128
+ */
129
+ .g6-toolbar.g6-toolbar-large .g6-toolbar-item:hover {
130
+ background-color: var(--bulma-background, #f0f0f0);
65
131
  }
66
132
 
67
133
  /* No transient effects: open/move popups immediately (user pref).