@yuneta/gobj-ui 7.3.0 → 7.4.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.
@@ -18595,6 +18595,7 @@ var PRIVATE_DATA$4 = {
18595
18595
  records: {},
18596
18596
  gobj_nodes_tree: null,
18597
18597
  find_timer: null,
18598
+ focus_topic: "",
18598
18599
  gobj_treedb_tables: null,
18599
18600
  hook_data_viewer: null,
18600
18601
  json_gobj: null,
@@ -18693,26 +18694,37 @@ function build_ui$4(gobj) {
18693
18694
  class: "C_YUI_TREEDB_GRAPH",
18694
18695
  style: `height:100%; display:flex; flex-direction:column;`
18695
18696
  },
18696
- [[
18697
- "div",
18698
- { class: "GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center" },
18699
- $back ? [$back, $toolbar] : [$toolbar]
18700
- ], [
18701
- "div",
18702
- {
18703
- class: `GRAPH_BODY is-flex-grow-1 ${padding}`,
18704
- style: "height:100%; min-height:0; overflow:hidden;"
18705
- },
18706
- [[
18697
+ [
18698
+ [
18699
+ "div",
18700
+ { class: "GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center" },
18701
+ $back ? [$back, $toolbar] : [$toolbar]
18702
+ ],
18703
+ [
18707
18704
  "div",
18708
18705
  {
18709
- id: priv.canvas_id,
18710
- class: `GRAPH_CANVAS graph-container`,
18711
- style: "height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;"
18706
+ class: "GRAPH_LEGEND is-hidden is-flex-grow-0",
18707
+ style: "display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; padding:.35rem .5rem;"
18712
18708
  },
18713
18709
  []
18714
- ]]
18715
- ]]
18710
+ ],
18711
+ [
18712
+ "div",
18713
+ {
18714
+ class: `GRAPH_BODY is-flex-grow-1 ${padding}`,
18715
+ style: "height:100%; min-height:0; overflow:hidden;"
18716
+ },
18717
+ [[
18718
+ "div",
18719
+ {
18720
+ id: priv.canvas_id,
18721
+ class: `GRAPH_CANVAS graph-container`,
18722
+ style: "height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;"
18723
+ },
18724
+ []
18725
+ ]]
18726
+ ]
18727
+ ]
18716
18728
  ]);
18717
18729
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", $container);
18718
18730
  (0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
@@ -18845,6 +18857,29 @@ function make_toolbar(gobj) {
18845
18857
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TREEDB", { evt }, gobj);
18846
18858
  } }
18847
18859
  ],
18860
+ [
18861
+ "button",
18862
+ {
18863
+ class: "GRAPH_LEGEND_BTN button ml-2",
18864
+ title: (0, i18next.t)("legend"),
18865
+ "aria-label": (0, i18next.t)("legend"),
18866
+ "data-i18n-title": "legend",
18867
+ "data-i18n-aria-label": "legend"
18868
+ },
18869
+ [["i", { class: "yi-square" }], [
18870
+ "span",
18871
+ {
18872
+ class: "is-hidden-mobile",
18873
+ style: "padding-left:5px;",
18874
+ i18n: "legend"
18875
+ },
18876
+ "legend"
18877
+ ]],
18878
+ { click: (evt) => {
18879
+ evt.stopPropagation();
18880
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_LEGEND", {}, gobj);
18881
+ } }
18882
+ ],
18848
18883
  [
18849
18884
  "button",
18850
18885
  {
@@ -19854,6 +19889,86 @@ function ac_set_operation_mode$1(gobj, event, kw, src) {
19854
19889
  * until its data is loaded).
19855
19890
  ************************************************************/
19856
19891
  /************************************************************
19892
+ * Fill the legend strip with one entry per topic: a swatch in the
19893
+ * topic's colour and its name. The colours belong to the graph child —
19894
+ * it assigns them from its palette when the schema arrives — so they
19895
+ * are read from there and never guessed twice.
19896
+ *
19897
+ * An entry is a BUTTON: clicking it focuses that topic, clicking the
19898
+ * focused one clears the focus. The focus travels the same way a topic
19899
+ * card's graph icon travels — as EV_TOPIC_SELECTED, which the host turns
19900
+ * into the URL, so what you are looking at stays linkable.
19901
+ ************************************************************/
19902
+ function refresh_legend(gobj) {
19903
+ let priv = gobj.priv;
19904
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
19905
+ if (!$container) return;
19906
+ let $legend = $container.querySelector(".GRAPH_LEGEND");
19907
+ if (!$legend) return;
19908
+ let descs = priv.gobj_nodes_tree ? (0, _yuneta_gobj_js.gobj_read_attr)(priv.gobj_nodes_tree, "descs") : null;
19909
+ $legend.textContent = "";
19910
+ if (!descs) return;
19911
+ for (const [topic_name, desc] of Object.entries(descs)) {
19912
+ if (topic_name.substring(0, 2) === "__") continue;
19913
+ if (!desc || !desc.color) continue;
19914
+ let focused = priv.focus_topic === topic_name;
19915
+ let $item = (0, _yuneta_gobj_js.createElement2)([
19916
+ "button",
19917
+ {
19918
+ class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " is-primary" : ""),
19919
+ title: topic_name,
19920
+ "aria-label": topic_name,
19921
+ "aria-pressed": focused ? "true" : "false"
19922
+ },
19923
+ [["span", {
19924
+ class: "GRAPH_LEGEND_SWATCH",
19925
+ style: `display:inline-block; width:.8rem; height:.8rem; border-radius:3px; margin-right:.4rem; background:${desc.color}; border:1px solid rgba(0,0,0,.25);`
19926
+ }], [
19927
+ "span",
19928
+ {},
19929
+ topic_name
19930
+ ]],
19931
+ { click: (evt) => {
19932
+ evt.stopPropagation();
19933
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_LEGEND_TOPIC", { topic: topic_name }, gobj);
19934
+ } }
19935
+ ]);
19936
+ $legend.appendChild($item);
19937
+ }
19938
+ }
19939
+ /************************************************************
19940
+ *
19941
+ ************************************************************/
19942
+ function ac_toggle_legend(gobj, event, kw, src) {
19943
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
19944
+ if (!$container) return 0;
19945
+ let $legend = $container.querySelector(".GRAPH_LEGEND");
19946
+ if (!$legend) {
19947
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no legend strip to toggle`);
19948
+ return -1;
19949
+ }
19950
+ if ($legend.classList.contains("is-hidden")) {
19951
+ refresh_legend(gobj);
19952
+ $legend.classList.remove("is-hidden");
19953
+ } else $legend.classList.add("is-hidden");
19954
+ return 0;
19955
+ }
19956
+ /************************************************************
19957
+ * A legend entry was clicked: focus that topic, or clear the focus if
19958
+ * it was already the focused one. Published UP rather than applied
19959
+ * here, so the URL follows — the host owns the route.
19960
+ ************************************************************/
19961
+ function ac_legend_topic(gobj, event, kw, src) {
19962
+ let priv = gobj.priv;
19963
+ let topic = kw && kw.topic || "";
19964
+ if (!topic) {
19965
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: legend click with no topic`);
19966
+ return -1;
19967
+ }
19968
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_TOPIC_SELECTED", { topic: priv.focus_topic === topic ? "" : topic });
19969
+ return 0;
19970
+ }
19971
+ /************************************************************
19857
19972
  * Forward the find down to the graph child.
19858
19973
  ************************************************************/
19859
19974
  function ac_find_nodes$1(gobj, event, kw, src) {
@@ -19882,7 +19997,11 @@ function ac_find_result(gobj, event, kw, src) {
19882
19997
  }
19883
19998
  function ac_set_focus_topic(gobj, event, kw, src) {
19884
19999
  let priv = gobj.priv;
19885
- if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_FOCUS_TOPIC", { topic: kw && kw.topic }, gobj);
20000
+ priv.focus_topic = kw && kw.topic || "";
20001
+ if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_FOCUS_TOPIC", { topic: priv.focus_topic }, gobj);
20002
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
20003
+ let $legend = $container ? $container.querySelector(".GRAPH_LEGEND") : null;
20004
+ if ($legend && !$legend.classList.contains("is-hidden")) refresh_legend(gobj);
19886
20005
  return 0;
19887
20006
  }
19888
20007
  /************************************************************
@@ -20050,6 +20169,16 @@ function create_gclass$4(gclass_name) {
20050
20169
  ac_find_nodes$1,
20051
20170
  null
20052
20171
  ],
20172
+ [
20173
+ "EV_TOGGLE_LEGEND",
20174
+ ac_toggle_legend,
20175
+ null
20176
+ ],
20177
+ [
20178
+ "EV_LEGEND_TOPIC",
20179
+ ac_legend_topic,
20180
+ null
20181
+ ],
20053
20182
  [
20054
20183
  "EV_FIND_RESULT",
20055
20184
  ac_find_result,
@@ -20098,6 +20227,9 @@ function create_gclass$4(gclass_name) {
20098
20227
  ["EV_SET_FOCUS_TOPIC", 0],
20099
20228
  ["EV_FIND_NODES", 0],
20100
20229
  ["EV_FIND_RESULT", 0],
20230
+ ["EV_TOGGLE_LEGEND", 0],
20231
+ ["EV_LEGEND_TOPIC", 0],
20232
+ ["EV_TOPIC_SELECTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
20101
20233
  ["EV_OPERATION_MODE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
20102
20234
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
20103
20235
  ["EV_SHOW", 0],
@@ -52704,6 +52836,7 @@ var attrs_table$1 = [
52704
52836
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_gridline", 0, true, "Use gridline plugin"),
52705
52837
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_fullscreen", 0, true, "Use fullscreen plugin"),
52706
52838
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_toolbar", 0, true, "Use toolbar plugin"),
52839
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "minimap_min_nodes", 0, 30, "Show the minimap from this many nodes on (0 = never). A minimap of a graph that fits on screen is decoration; one of two hundred cards is the only way to know where you are"),
52707
52840
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "toolbar_position", 0, "right-top", "Toolbar position: top-left, top-right, bottom-left, bottom-right, left-top, right-top"),
52708
52841
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_LIST, "layout_names", _yuneta_gobj_js.sdata_flag_t.SDF_RD, JSON.stringify(Object.keys(_layouts)), "Available layout names (read-only, for parent to query)"),
52709
52842
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
@@ -55543,6 +55676,61 @@ function show_node_detail_popover(gobj, node_id) {
55543
55676
  * junction diamonds get their neutral fill/stroke re-themed.
55544
55677
  ************************************************************/
55545
55678
  /************************************************************
55679
+ * Show or hide the minimap, deciding by the SIZE of the graph.
55680
+ *
55681
+ * A minimap of a graph that already fits on screen is decoration; one
55682
+ * of two hundred cards is the only way to know where you are. So it is
55683
+ * not a preference the reader has to find and set — it appears when
55684
+ * there is something to be lost in, from `minimap_min_nodes` on.
55685
+ *
55686
+ * Its shapes are drawn by hand. G6's minimap clones each element's key
55687
+ * shape into its own canvas, and every node here is an `html` node
55688
+ * whose key shape is a DOM element — the same reason the `active`
55689
+ * state never painted (see HIGHLIGHT_COLOR). A block in the topic's
55690
+ * colour is also what a minimap of cards SHOULD show: at that scale a
55691
+ * card is a rectangle anyway.
55692
+ ************************************************************/
55693
+ function refresh_minimap(gobj) {
55694
+ let graph = gobj.priv.graph;
55695
+ if (!graph) return;
55696
+ let threshold = (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "minimap_min_nodes");
55697
+ let n = 0;
55698
+ try {
55699
+ let data = graph.getData() || {};
55700
+ n = (0, _yuneta_gobj_js.is_array)(data.nodes) ? data.nodes.length : 0;
55701
+ } catch (e) {
55702
+ n = 0;
55703
+ }
55704
+ if (!threshold || n < threshold) {
55705
+ graph_remove_plugin(gobj, "minimap");
55706
+ return;
55707
+ }
55708
+ if (graph_get_plugin(gobj, "minimap")) return;
55709
+ graph_add_plugin(gobj, "minimap", {
55710
+ size: [200, 140],
55711
+ position: "bottom-left",
55712
+ shape: (id, element_type, element) => {
55713
+ if (element_type !== "node") return element;
55714
+ try {
55715
+ let nd = graph.getNodeData(id);
55716
+ let size = nd && nd.style && nd.style.size || [120, 60];
55717
+ let color = nd && nd.data && nd.data.desc && nd.data.desc.color || "#94a3b8";
55718
+ return new Rect({ style: {
55719
+ x: -size[0] / 2,
55720
+ y: -size[1] / 2,
55721
+ width: size[0],
55722
+ height: size[1],
55723
+ fill: color,
55724
+ radius: 4
55725
+ } });
55726
+ } catch (e) {
55727
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: minimap shape failed: ${e}`);
55728
+ return element;
55729
+ }
55730
+ }
55731
+ });
55732
+ }
55733
+ /************************************************************
55546
55734
  * The innerHTML of ONE node, in the given theme and highlight state.
55547
55735
  * The three treedb tiers (hierarchical / extended / child) each have
55548
55736
  * their own card, and both the theme refresh and the highlight need the
@@ -56339,6 +56527,7 @@ function ac_load_data(gobj, event, kw, src) {
56339
56527
  graph_layout(gobj).then(() => {
56340
56528
  if (priv.edit_mode) graph_add_plugin(gobj, "history");
56341
56529
  else graph_remove_plugin(gobj, "history");
56530
+ refresh_minimap(gobj);
56342
56531
  if (priv._pending_focus_topic !== null) {
56343
56532
  let ft = priv._pending_focus_topic;
56344
56533
  priv._pending_focus_topic = null;
@@ -18582,6 +18582,7 @@ var PRIVATE_DATA$4 = {
18582
18582
  records: {},
18583
18583
  gobj_nodes_tree: null,
18584
18584
  find_timer: null,
18585
+ focus_topic: "",
18585
18586
  gobj_treedb_tables: null,
18586
18587
  hook_data_viewer: null,
18587
18588
  json_gobj: null,
@@ -18680,26 +18681,37 @@ function build_ui$4(gobj) {
18680
18681
  class: "C_YUI_TREEDB_GRAPH",
18681
18682
  style: `height:100%; display:flex; flex-direction:column;`
18682
18683
  },
18683
- [[
18684
- "div",
18685
- { class: "GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center" },
18686
- $back ? [$back, $toolbar] : [$toolbar]
18687
- ], [
18688
- "div",
18689
- {
18690
- class: `GRAPH_BODY is-flex-grow-1 ${padding}`,
18691
- style: "height:100%; min-height:0; overflow:hidden;"
18692
- },
18693
- [[
18684
+ [
18685
+ [
18686
+ "div",
18687
+ { class: "GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center" },
18688
+ $back ? [$back, $toolbar] : [$toolbar]
18689
+ ],
18690
+ [
18694
18691
  "div",
18695
18692
  {
18696
- id: priv.canvas_id,
18697
- class: `GRAPH_CANVAS graph-container`,
18698
- style: "height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;"
18693
+ class: "GRAPH_LEGEND is-hidden is-flex-grow-0",
18694
+ style: "display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; padding:.35rem .5rem;"
18699
18695
  },
18700
18696
  []
18701
- ]]
18702
- ]]
18697
+ ],
18698
+ [
18699
+ "div",
18700
+ {
18701
+ class: `GRAPH_BODY is-flex-grow-1 ${padding}`,
18702
+ style: "height:100%; min-height:0; overflow:hidden;"
18703
+ },
18704
+ [[
18705
+ "div",
18706
+ {
18707
+ id: priv.canvas_id,
18708
+ class: `GRAPH_CANVAS graph-container`,
18709
+ style: "height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;"
18710
+ },
18711
+ []
18712
+ ]]
18713
+ ]
18714
+ ]
18703
18715
  ]);
18704
18716
  gobj_write_attr(gobj, "$container", $container);
18705
18717
  refresh_language($container, t);
@@ -18832,6 +18844,29 @@ function make_toolbar(gobj) {
18832
18844
  gobj_send_event(gobj, "EV_REFRESH_TREEDB", { evt }, gobj);
18833
18845
  } }
18834
18846
  ],
18847
+ [
18848
+ "button",
18849
+ {
18850
+ class: "GRAPH_LEGEND_BTN button ml-2",
18851
+ title: t("legend"),
18852
+ "aria-label": t("legend"),
18853
+ "data-i18n-title": "legend",
18854
+ "data-i18n-aria-label": "legend"
18855
+ },
18856
+ [["i", { class: "yi-square" }], [
18857
+ "span",
18858
+ {
18859
+ class: "is-hidden-mobile",
18860
+ style: "padding-left:5px;",
18861
+ i18n: "legend"
18862
+ },
18863
+ "legend"
18864
+ ]],
18865
+ { click: (evt) => {
18866
+ evt.stopPropagation();
18867
+ gobj_send_event(gobj, "EV_TOGGLE_LEGEND", {}, gobj);
18868
+ } }
18869
+ ],
18835
18870
  [
18836
18871
  "button",
18837
18872
  {
@@ -19841,6 +19876,86 @@ function ac_set_operation_mode$1(gobj, event, kw, src) {
19841
19876
  * until its data is loaded).
19842
19877
  ************************************************************/
19843
19878
  /************************************************************
19879
+ * Fill the legend strip with one entry per topic: a swatch in the
19880
+ * topic's colour and its name. The colours belong to the graph child —
19881
+ * it assigns them from its palette when the schema arrives — so they
19882
+ * are read from there and never guessed twice.
19883
+ *
19884
+ * An entry is a BUTTON: clicking it focuses that topic, clicking the
19885
+ * focused one clears the focus. The focus travels the same way a topic
19886
+ * card's graph icon travels — as EV_TOPIC_SELECTED, which the host turns
19887
+ * into the URL, so what you are looking at stays linkable.
19888
+ ************************************************************/
19889
+ function refresh_legend(gobj) {
19890
+ let priv = gobj.priv;
19891
+ let $container = gobj_read_attr(gobj, "$container");
19892
+ if (!$container) return;
19893
+ let $legend = $container.querySelector(".GRAPH_LEGEND");
19894
+ if (!$legend) return;
19895
+ let descs = priv.gobj_nodes_tree ? gobj_read_attr(priv.gobj_nodes_tree, "descs") : null;
19896
+ $legend.textContent = "";
19897
+ if (!descs) return;
19898
+ for (const [topic_name, desc] of Object.entries(descs)) {
19899
+ if (topic_name.substring(0, 2) === "__") continue;
19900
+ if (!desc || !desc.color) continue;
19901
+ let focused = priv.focus_topic === topic_name;
19902
+ let $item = createElement2([
19903
+ "button",
19904
+ {
19905
+ class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " is-primary" : ""),
19906
+ title: topic_name,
19907
+ "aria-label": topic_name,
19908
+ "aria-pressed": focused ? "true" : "false"
19909
+ },
19910
+ [["span", {
19911
+ class: "GRAPH_LEGEND_SWATCH",
19912
+ style: `display:inline-block; width:.8rem; height:.8rem; border-radius:3px; margin-right:.4rem; background:${desc.color}; border:1px solid rgba(0,0,0,.25);`
19913
+ }], [
19914
+ "span",
19915
+ {},
19916
+ topic_name
19917
+ ]],
19918
+ { click: (evt) => {
19919
+ evt.stopPropagation();
19920
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC", { topic: topic_name }, gobj);
19921
+ } }
19922
+ ]);
19923
+ $legend.appendChild($item);
19924
+ }
19925
+ }
19926
+ /************************************************************
19927
+ *
19928
+ ************************************************************/
19929
+ function ac_toggle_legend(gobj, event, kw, src) {
19930
+ let $container = gobj_read_attr(gobj, "$container");
19931
+ if (!$container) return 0;
19932
+ let $legend = $container.querySelector(".GRAPH_LEGEND");
19933
+ if (!$legend) {
19934
+ log_error(`${gobj_short_name(gobj)}: no legend strip to toggle`);
19935
+ return -1;
19936
+ }
19937
+ if ($legend.classList.contains("is-hidden")) {
19938
+ refresh_legend(gobj);
19939
+ $legend.classList.remove("is-hidden");
19940
+ } else $legend.classList.add("is-hidden");
19941
+ return 0;
19942
+ }
19943
+ /************************************************************
19944
+ * A legend entry was clicked: focus that topic, or clear the focus if
19945
+ * it was already the focused one. Published UP rather than applied
19946
+ * here, so the URL follows — the host owns the route.
19947
+ ************************************************************/
19948
+ function ac_legend_topic(gobj, event, kw, src) {
19949
+ let priv = gobj.priv;
19950
+ let topic = kw && kw.topic || "";
19951
+ if (!topic) {
19952
+ log_error(`${gobj_short_name(gobj)}: legend click with no topic`);
19953
+ return -1;
19954
+ }
19955
+ gobj_publish_event(gobj, "EV_TOPIC_SELECTED", { topic: priv.focus_topic === topic ? "" : topic });
19956
+ return 0;
19957
+ }
19958
+ /************************************************************
19844
19959
  * Forward the find down to the graph child.
19845
19960
  ************************************************************/
19846
19961
  function ac_find_nodes$1(gobj, event, kw, src) {
@@ -19869,7 +19984,11 @@ function ac_find_result(gobj, event, kw, src) {
19869
19984
  }
19870
19985
  function ac_set_focus_topic(gobj, event, kw, src) {
19871
19986
  let priv = gobj.priv;
19872
- if (priv.gobj_nodes_tree) gobj_send_event(priv.gobj_nodes_tree, "EV_FOCUS_TOPIC", { topic: kw && kw.topic }, gobj);
19987
+ priv.focus_topic = kw && kw.topic || "";
19988
+ if (priv.gobj_nodes_tree) gobj_send_event(priv.gobj_nodes_tree, "EV_FOCUS_TOPIC", { topic: priv.focus_topic }, gobj);
19989
+ let $container = gobj_read_attr(gobj, "$container");
19990
+ let $legend = $container ? $container.querySelector(".GRAPH_LEGEND") : null;
19991
+ if ($legend && !$legend.classList.contains("is-hidden")) refresh_legend(gobj);
19873
19992
  return 0;
19874
19993
  }
19875
19994
  /************************************************************
@@ -20037,6 +20156,16 @@ function create_gclass$4(gclass_name) {
20037
20156
  ac_find_nodes$1,
20038
20157
  null
20039
20158
  ],
20159
+ [
20160
+ "EV_TOGGLE_LEGEND",
20161
+ ac_toggle_legend,
20162
+ null
20163
+ ],
20164
+ [
20165
+ "EV_LEGEND_TOPIC",
20166
+ ac_legend_topic,
20167
+ null
20168
+ ],
20040
20169
  [
20041
20170
  "EV_FIND_RESULT",
20042
20171
  ac_find_result,
@@ -20085,6 +20214,9 @@ function create_gclass$4(gclass_name) {
20085
20214
  ["EV_SET_FOCUS_TOPIC", 0],
20086
20215
  ["EV_FIND_NODES", 0],
20087
20216
  ["EV_FIND_RESULT", 0],
20217
+ ["EV_TOGGLE_LEGEND", 0],
20218
+ ["EV_LEGEND_TOPIC", 0],
20219
+ ["EV_TOPIC_SELECTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
20088
20220
  ["EV_OPERATION_MODE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
20089
20221
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
20090
20222
  ["EV_SHOW", 0],
@@ -52691,6 +52823,7 @@ var attrs_table$1 = [
52691
52823
  SDATA(data_type_t.DTP_BOOLEAN, "with_gridline", 0, true, "Use gridline plugin"),
52692
52824
  SDATA(data_type_t.DTP_BOOLEAN, "with_fullscreen", 0, true, "Use fullscreen plugin"),
52693
52825
  SDATA(data_type_t.DTP_BOOLEAN, "with_toolbar", 0, true, "Use toolbar plugin"),
52826
+ SDATA(data_type_t.DTP_INTEGER, "minimap_min_nodes", 0, 30, "Show the minimap from this many nodes on (0 = never). A minimap of a graph that fits on screen is decoration; one of two hundred cards is the only way to know where you are"),
52694
52827
  SDATA(data_type_t.DTP_STRING, "toolbar_position", 0, "right-top", "Toolbar position: top-left, top-right, bottom-left, bottom-right, left-top, right-top"),
52695
52828
  SDATA(data_type_t.DTP_LIST, "layout_names", sdata_flag_t.SDF_RD, JSON.stringify(Object.keys(_layouts)), "Available layout names (read-only, for parent to query)"),
52696
52829
  SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook port position"),
@@ -55530,6 +55663,61 @@ function show_node_detail_popover(gobj, node_id) {
55530
55663
  * junction diamonds get their neutral fill/stroke re-themed.
55531
55664
  ************************************************************/
55532
55665
  /************************************************************
55666
+ * Show or hide the minimap, deciding by the SIZE of the graph.
55667
+ *
55668
+ * A minimap of a graph that already fits on screen is decoration; one
55669
+ * of two hundred cards is the only way to know where you are. So it is
55670
+ * not a preference the reader has to find and set — it appears when
55671
+ * there is something to be lost in, from `minimap_min_nodes` on.
55672
+ *
55673
+ * Its shapes are drawn by hand. G6's minimap clones each element's key
55674
+ * shape into its own canvas, and every node here is an `html` node
55675
+ * whose key shape is a DOM element — the same reason the `active`
55676
+ * state never painted (see HIGHLIGHT_COLOR). A block in the topic's
55677
+ * colour is also what a minimap of cards SHOULD show: at that scale a
55678
+ * card is a rectangle anyway.
55679
+ ************************************************************/
55680
+ function refresh_minimap(gobj) {
55681
+ let graph = gobj.priv.graph;
55682
+ if (!graph) return;
55683
+ let threshold = gobj_read_integer_attr(gobj, "minimap_min_nodes");
55684
+ let n = 0;
55685
+ try {
55686
+ let data = graph.getData() || {};
55687
+ n = is_array(data.nodes) ? data.nodes.length : 0;
55688
+ } catch (e) {
55689
+ n = 0;
55690
+ }
55691
+ if (!threshold || n < threshold) {
55692
+ graph_remove_plugin(gobj, "minimap");
55693
+ return;
55694
+ }
55695
+ if (graph_get_plugin(gobj, "minimap")) return;
55696
+ graph_add_plugin(gobj, "minimap", {
55697
+ size: [200, 140],
55698
+ position: "bottom-left",
55699
+ shape: (id, element_type, element) => {
55700
+ if (element_type !== "node") return element;
55701
+ try {
55702
+ let nd = graph.getNodeData(id);
55703
+ let size = nd && nd.style && nd.style.size || [120, 60];
55704
+ let color = nd && nd.data && nd.data.desc && nd.data.desc.color || "#94a3b8";
55705
+ return new Rect({ style: {
55706
+ x: -size[0] / 2,
55707
+ y: -size[1] / 2,
55708
+ width: size[0],
55709
+ height: size[1],
55710
+ fill: color,
55711
+ radius: 4
55712
+ } });
55713
+ } catch (e) {
55714
+ log_error(`${gobj_short_name(gobj)}: minimap shape failed: ${e}`);
55715
+ return element;
55716
+ }
55717
+ }
55718
+ });
55719
+ }
55720
+ /************************************************************
55533
55721
  * The innerHTML of ONE node, in the given theme and highlight state.
55534
55722
  * The three treedb tiers (hierarchical / extended / child) each have
55535
55723
  * their own card, and both the theme refresh and the highlight need the
@@ -56326,6 +56514,7 @@ function ac_load_data(gobj, event, kw, src) {
56326
56514
  graph_layout(gobj).then(() => {
56327
56515
  if (priv.edit_mode) graph_add_plugin(gobj, "history");
56328
56516
  else graph_remove_plugin(gobj, "history");
56517
+ refresh_minimap(gobj);
56329
56518
  if (priv._pending_focus_topic !== null) {
56330
56519
  let ft = priv._pending_focus_topic;
56331
56520
  priv._pending_focus_topic = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -31,6 +31,7 @@ import {
31
31
  gobj_read_attr,
32
32
  gobj_read_str_attr,
33
33
  gobj_read_bool_attr,
34
+ gobj_read_integer_attr,
34
35
  gobj_write_attr,
35
36
  gobj_write_str_attr,
36
37
  gobj_parent,
@@ -93,7 +94,7 @@ import {
93
94
  register,
94
95
  } from '@antv/g6';
95
96
 
96
- import {Circle as CircleGeometry} from '@antv/g';
97
+ import {Circle as CircleGeometry, Rect as RectGeometry} from '@antv/g';
97
98
  import i18next, {t} from "i18next";
98
99
 
99
100
  import {inject_svg_icons} from "./lib_icons.js";
@@ -218,6 +219,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables", 0, false, "Include tre
218
219
  SDATA(data_type_t.DTP_BOOLEAN, "with_gridline", 0, true, "Use gridline plugin"),
219
220
  SDATA(data_type_t.DTP_BOOLEAN, "with_fullscreen", 0, true, "Use fullscreen plugin"),
220
221
  SDATA(data_type_t.DTP_BOOLEAN, "with_toolbar", 0, true, "Use toolbar plugin"),
222
+ SDATA(data_type_t.DTP_INTEGER, "minimap_min_nodes", 0, 30, "Show the minimap from this many nodes on (0 = never). A minimap of a graph that fits on screen is decoration; one of two hundred cards is the only way to know where you are"),
221
223
  SDATA(data_type_t.DTP_STRING, "toolbar_position", 0, "right-top",
222
224
  "Toolbar position: top-left, top-right, bottom-left, bottom-right, left-top, right-top"),
223
225
  SDATA(data_type_t.DTP_LIST, "layout_names", sdata_flag_t.SDF_RD,
@@ -4481,6 +4483,81 @@ function show_node_detail_popover(gobj, node_id)
4481
4483
  * not re-themed by setTheme()) are regenerated; structural
4482
4484
  * junction diamonds get their neutral fill/stroke re-themed.
4483
4485
  ************************************************************/
4486
+ /************************************************************
4487
+ * Show or hide the minimap, deciding by the SIZE of the graph.
4488
+ *
4489
+ * A minimap of a graph that already fits on screen is decoration; one
4490
+ * of two hundred cards is the only way to know where you are. So it is
4491
+ * not a preference the reader has to find and set — it appears when
4492
+ * there is something to be lost in, from `minimap_min_nodes` on.
4493
+ *
4494
+ * Its shapes are drawn by hand. G6's minimap clones each element's key
4495
+ * shape into its own canvas, and every node here is an `html` node
4496
+ * whose key shape is a DOM element — the same reason the `active`
4497
+ * state never painted (see HIGHLIGHT_COLOR). A block in the topic's
4498
+ * colour is also what a minimap of cards SHOULD show: at that scale a
4499
+ * card is a rectangle anyway.
4500
+ ************************************************************/
4501
+ function refresh_minimap(gobj)
4502
+ {
4503
+ let priv = gobj.priv;
4504
+ let graph = priv.graph;
4505
+ if(!graph) {
4506
+ return;
4507
+ }
4508
+
4509
+ let threshold = gobj_read_integer_attr(gobj, "minimap_min_nodes");
4510
+ let n = 0;
4511
+ try {
4512
+ let data = graph.getData() || {};
4513
+ n = is_array(data.nodes)? data.nodes.length : 0;
4514
+ } catch(e) {
4515
+ n = 0;
4516
+ }
4517
+
4518
+ if(!threshold || n < threshold) {
4519
+ graph_remove_plugin(gobj, "minimap");
4520
+ return;
4521
+ }
4522
+
4523
+ if(graph_get_plugin(gobj, "minimap")) {
4524
+ return; /* already up */
4525
+ }
4526
+
4527
+ graph_add_plugin(
4528
+ gobj,
4529
+ "minimap",
4530
+ {
4531
+ size: [200, 140],
4532
+ position: "bottom-left",
4533
+ shape: (id, element_type, element) => {
4534
+ if(element_type !== "node") {
4535
+ return element; /* edges clone themselves fine */
4536
+ }
4537
+ try {
4538
+ let nd = graph.getNodeData(id);
4539
+ let size = (nd && nd.style && nd.style.size) || [120, 60];
4540
+ let color = (nd && nd.data && nd.data.desc && nd.data.desc.color)
4541
+ || "#94a3b8";
4542
+ return new RectGeometry({
4543
+ style: {
4544
+ x: -size[0] / 2,
4545
+ y: -size[1] / 2,
4546
+ width: size[0],
4547
+ height: size[1],
4548
+ fill: color,
4549
+ radius: 4
4550
+ }
4551
+ });
4552
+ } catch(e) {
4553
+ log_error(`${gobj_short_name(gobj)}: minimap shape failed: ${e}`);
4554
+ return element;
4555
+ }
4556
+ }
4557
+ }
4558
+ );
4559
+ }
4560
+
4484
4561
  /************************************************************
4485
4562
  * The innerHTML of ONE node, in the given theme and highlight state.
4486
4563
  * The three treedb tiers (hierarchical / extended / child) each have
@@ -5686,6 +5763,9 @@ function ac_load_data(gobj, event, kw, src)
5686
5763
  } else {
5687
5764
  graph_remove_plugin(gobj, "history");
5688
5765
  }
5766
+ /* The graph knows its size now: a minimap appears only
5767
+ * when there is something to be lost in. */
5768
+ refresh_minimap(gobj);
5689
5769
  /* Nodes exist and are positioned now: apply a focus
5690
5770
  * requested before the data was ready (deep link). */
5691
5771
  if(priv._pending_focus_topic !== null) {
@@ -148,6 +148,7 @@ let PRIVATE_DATA = {
148
148
  records: {},
149
149
  gobj_nodes_tree: null,
150
150
  find_timer: null, // rate-limits the find box (see make_toolbar)
151
+ focus_topic: "", // topic the graph is focused on (marks the legend)
151
152
  gobj_treedb_tables: null,
152
153
  hook_data_viewer: null,
153
154
  json_gobj: null, /* C_YUI_JSON viewer of the raw tranger (or null) */
@@ -354,6 +355,13 @@ function build_ui(gobj)
354
355
  // Don't use is-flex, don't work well with is-hidden
355
356
  ['div', {class: 'C_YUI_TREEDB_GRAPH', style: `height:100%; display:flex; flex-direction:column;`}, [
356
357
  ['div', {class: 'GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center'}, row_items],
358
+ /* Topic colour legend: a strip, not an overlay. It is opened to
359
+ * be READ against the graph, and an overlay would cover the
360
+ * thing it explains. Built on first open (the colours are the
361
+ * child's, assigned when the schema arrives). */
362
+ ['div', {class: 'GRAPH_LEGEND is-hidden is-flex-grow-0',
363
+ style: 'display:flex; flex-wrap:wrap; align-items:center; ' +
364
+ 'gap:.4rem; padding:.35rem .5rem;'}, []],
357
365
  ['div', {class: `GRAPH_BODY is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
358
366
  ['div', {id: priv.canvas_id, class: `GRAPH_CANVAS graph-container`, style: 'height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;'}, [
359
367
  ]]
@@ -496,6 +504,23 @@ function make_toolbar(gobj)
496
504
  }
497
505
  }],
498
506
 
507
+ /* Which colour is which topic. The port colour of a node encodes
508
+ * the topic it links to, which is the whole point of the graph and
509
+ * which nothing on screen explained. Clicking an entry focuses that
510
+ * topic, so the legend is also the way to say "show me these" —
511
+ * and clicking the focused one again clears it. */
512
+ ['button', {class: 'GRAPH_LEGEND_BTN button ml-2',
513
+ title: t('legend'), 'aria-label': t('legend'),
514
+ 'data-i18n-title': 'legend', 'data-i18n-aria-label': 'legend'}, [
515
+ ['i', {class: 'yi-square'}],
516
+ ['span', {class: 'is-hidden-mobile', style: 'padding-left:5px;', i18n: 'legend'}, 'legend']
517
+ ], {
518
+ click: (evt) => {
519
+ evt.stopPropagation();
520
+ gobj_send_event(gobj, "EV_TOGGLE_LEGEND", {}, gobj);
521
+ }
522
+ }],
523
+
499
524
  /* Inspect the treedb's raw tranger json in the lazy tree viewer
500
525
  * (print-tranger on the C_NODE service). A treedb can be huge, so
501
526
  * the viewer drills in on demand — see open_json_viewer. */
@@ -2194,6 +2219,115 @@ function ac_set_operation_mode(gobj, event, kw, src)
2194
2219
  * tree, which centres + highlights that topic's nodes (deferring
2195
2220
  * until its data is loaded).
2196
2221
  ************************************************************/
2222
+ /************************************************************
2223
+ * Fill the legend strip with one entry per topic: a swatch in the
2224
+ * topic's colour and its name. The colours belong to the graph child —
2225
+ * it assigns them from its palette when the schema arrives — so they
2226
+ * are read from there and never guessed twice.
2227
+ *
2228
+ * An entry is a BUTTON: clicking it focuses that topic, clicking the
2229
+ * focused one clears the focus. The focus travels the same way a topic
2230
+ * card's graph icon travels — as EV_TOPIC_SELECTED, which the host turns
2231
+ * into the URL, so what you are looking at stays linkable.
2232
+ ************************************************************/
2233
+ function refresh_legend(gobj)
2234
+ {
2235
+ let priv = gobj.priv;
2236
+ let $container = gobj_read_attr(gobj, "$container");
2237
+ if(!$container) {
2238
+ return;
2239
+ }
2240
+ let $legend = $container.querySelector(".GRAPH_LEGEND");
2241
+ if(!$legend) {
2242
+ return;
2243
+ }
2244
+
2245
+ let descs = priv.gobj_nodes_tree?
2246
+ gobj_read_attr(priv.gobj_nodes_tree, "descs") : null;
2247
+ $legend.textContent = "";
2248
+ if(!descs) {
2249
+ return;
2250
+ }
2251
+
2252
+ for(const [topic_name, desc] of Object.entries(descs)) {
2253
+ if(topic_name.substring(0, 2) === "__") {
2254
+ continue; /* the internal topics are not drawn */
2255
+ }
2256
+ if(!desc || !desc.color) {
2257
+ continue;
2258
+ }
2259
+ let focused = (priv.focus_topic === topic_name);
2260
+ let $item = createElement2(
2261
+ ['button', {
2262
+ class: 'GRAPH_LEGEND_ITEM button is-small' +
2263
+ (focused? ' is-primary' : ''),
2264
+ title: topic_name,
2265
+ 'aria-label': topic_name,
2266
+ 'aria-pressed': focused? 'true' : 'false'
2267
+ }, [
2268
+ ['span', {class: 'GRAPH_LEGEND_SWATCH',
2269
+ style: `display:inline-block; width:.8rem; height:.8rem; ` +
2270
+ `border-radius:3px; margin-right:.4rem; ` +
2271
+ `background:${desc.color}; ` +
2272
+ `border:1px solid rgba(0,0,0,.25);`}],
2273
+ /* The topic name is DATA: it carries no i18n key and is
2274
+ * never translated. */
2275
+ ['span', {}, topic_name]
2276
+ ], {
2277
+ click: (evt) => {
2278
+ evt.stopPropagation();
2279
+ gobj_send_event(gobj, "EV_LEGEND_TOPIC", {topic: topic_name}, gobj);
2280
+ }
2281
+ }]
2282
+ );
2283
+ $legend.appendChild($item);
2284
+ }
2285
+ }
2286
+
2287
+ /************************************************************
2288
+ *
2289
+ ************************************************************/
2290
+ function ac_toggle_legend(gobj, event, kw, src)
2291
+ {
2292
+ let $container = gobj_read_attr(gobj, "$container");
2293
+ if(!$container) {
2294
+ return 0;
2295
+ }
2296
+ let $legend = $container.querySelector(".GRAPH_LEGEND");
2297
+ if(!$legend) {
2298
+ log_error(`${gobj_short_name(gobj)}: no legend strip to toggle`);
2299
+ return -1;
2300
+ }
2301
+ if($legend.classList.contains("is-hidden")) {
2302
+ refresh_legend(gobj);
2303
+ $legend.classList.remove("is-hidden");
2304
+ } else {
2305
+ $legend.classList.add("is-hidden");
2306
+ }
2307
+ return 0;
2308
+ }
2309
+
2310
+ /************************************************************
2311
+ * A legend entry was clicked: focus that topic, or clear the focus if
2312
+ * it was already the focused one. Published UP rather than applied
2313
+ * here, so the URL follows — the host owns the route.
2314
+ ************************************************************/
2315
+ function ac_legend_topic(gobj, event, kw, src)
2316
+ {
2317
+ let priv = gobj.priv;
2318
+ let topic = (kw && kw.topic) || "";
2319
+ if(!topic) {
2320
+ log_error(`${gobj_short_name(gobj)}: legend click with no topic`);
2321
+ return -1;
2322
+ }
2323
+ gobj_publish_event(
2324
+ gobj,
2325
+ "EV_TOPIC_SELECTED",
2326
+ {topic: (priv.focus_topic === topic)? "" : topic}
2327
+ );
2328
+ return 0;
2329
+ }
2330
+
2197
2331
  /************************************************************
2198
2332
  * Forward the find down to the graph child.
2199
2333
  ************************************************************/
@@ -2242,14 +2376,22 @@ function ac_find_result(gobj, event, kw, src)
2242
2376
  function ac_set_focus_topic(gobj, event, kw, src)
2243
2377
  {
2244
2378
  let priv = gobj.priv;
2379
+ priv.focus_topic = (kw && kw.topic) || "";
2245
2380
  if(priv.gobj_nodes_tree) {
2246
2381
  gobj_send_event(
2247
2382
  priv.gobj_nodes_tree,
2248
2383
  "EV_FOCUS_TOPIC",
2249
- {topic: kw && kw.topic},
2384
+ {topic: priv.focus_topic},
2250
2385
  gobj
2251
2386
  );
2252
2387
  }
2388
+ /* Keep the legend's mark on the topic that is actually focused —
2389
+ * including when the focus arrives from the URL and not from a click. */
2390
+ let $container = gobj_read_attr(gobj, "$container");
2391
+ let $legend = $container? $container.querySelector(".GRAPH_LEGEND") : null;
2392
+ if($legend && !$legend.classList.contains("is-hidden")) {
2393
+ refresh_legend(gobj);
2394
+ }
2253
2395
  return 0;
2254
2396
  }
2255
2397
 
@@ -2339,6 +2481,8 @@ function create_gclass(gclass_name)
2339
2481
  ["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
2340
2482
  ["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
2341
2483
  ["EV_FIND_NODES", ac_find_nodes, null],
2484
+ ["EV_TOGGLE_LEGEND", ac_toggle_legend, null],
2485
+ ["EV_LEGEND_TOPIC", ac_legend_topic, null],
2342
2486
  ["EV_FIND_RESULT", ac_find_result, null],
2343
2487
  ["EV_SHOW", ac_show, null],
2344
2488
  ["EV_HIDE", ac_hide, null],
@@ -2376,6 +2520,10 @@ function create_gclass(gclass_name)
2376
2520
  ["EV_SET_FOCUS_TOPIC", 0],
2377
2521
  ["EV_FIND_NODES", 0],
2378
2522
  ["EV_FIND_RESULT", 0],
2523
+ ["EV_TOGGLE_LEGEND", 0],
2524
+ ["EV_LEGEND_TOPIC", 0],
2525
+ ["EV_TOPIC_SELECTED",
2526
+ event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2379
2527
  ["EV_OPERATION_MODE_CHANGED",
2380
2528
  event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
2381
2529
  ["EV_RECORD_WRITTEN",