@yuneta/gobj-ui 7.23.148 → 7.23.150

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.
@@ -134,6 +134,7 @@ SDATA(data_type_t.DTP_LIST, "hidden_topics", sdata_flag_t.SDF_PERSIST, "[
134
134
  SDATA(data_type_t.DTP_STRING, "main_topic", sdata_flag_t.SDF_PERSIST, "", "The topic the tree hangs from; empty = deduced by the graph. User preference, per treedb"),
135
135
  SDATA(data_type_t.DTP_LIST, "loose_topics", sdata_flag_t.SDF_PERSIST, "[]", "Topics whose LOOSE records (no parent) are shown. User preference, per treedb"),
136
136
  SDATA(data_type_t.DTP_STRING, "node_mode", sdata_flag_t.SDF_PERSIST, "expanded", "How the records are drawn: `expanded` = the cards with their pills and ports, `compact` = one-line pills with the name, `shape` = coloured figures, the topology alone. User preference"),
137
+ SDATA(data_type_t.DTP_STRING, "edge_shape", sdata_flag_t.SDF_PERSIST, "curved", "How the edges are drawn: `curved` or `elbow` (the bus of an org chart). User preference"),
137
138
  SDATA(data_type_t.DTP_BOOLEAN, "node_labels", sdata_flag_t.SDF_PERSIST, true, "A `shape` node says its name under its figure. User preference"),
138
139
  SDATA(data_type_t.DTP_DICT, "camera", sdata_flag_t.SDF_PERSIST, "{}", "Where the reader left the viewport: {zoom, x, y}. Handed to the engine on creation and written back when a move settles, so the graph opens where it was left. User preference, per treedb"),
139
140
  SDATA(data_type_t.DTP_INTEGER, "fold_level", sdata_flag_t.SDF_PERSIST, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = the main topic's roots, N = N-1 levels under them; 0 = never stepped, `expand_depth`. Handed to the engine on creation, so the graph opens at the level it was left. User preference, per treedb"),
@@ -283,6 +284,7 @@ function mt_create(gobj)
283
284
  loose_topics: gobj_read_attr(gobj, "loose_topics") || [],
284
285
  node_mode: gobj_read_str_attr(gobj, "node_mode") || "expanded",
285
286
  node_labels: gobj_read_bool_attr(gobj, "node_labels"),
287
+ edge_shape: gobj_read_str_attr(gobj, "edge_shape") || "curved",
286
288
  camera: gobj_read_attr(gobj, "camera") || {},
287
289
  },
288
290
  gobj
@@ -625,6 +627,22 @@ function node_shape_items(gobj, wide)
625
627
  }
626
628
  }
627
629
  ],
630
+ /* How the lines are drawn: curves, or the elbows of an org
631
+ * chart. A toggle, so a pressed pill; disabled on a layout
632
+ * with no rows, where the edges stay curved anyway. */
633
+ ['button', {class: 'GRAPH_EDGE_ELBOW button', type: 'button',
634
+ style: {height: wide, width: '2.5em'},
635
+ title: t('elbow edges'), 'data-i18n-title': 'elbow edges',
636
+ 'aria-label': t('elbow edges'), 'data-i18n-aria-label': 'elbow edges',
637
+ 'aria-pressed': 'false'},
638
+ yui_toolbar_icon('yi-diagram-project'),
639
+ {
640
+ click: (evt) => {
641
+ evt.stopPropagation();
642
+ gobj_send_event(gobj, "EV_TOGGLE_EDGE_SHAPE", {}, gobj);
643
+ }
644
+ }
645
+ ],
628
646
  ];
629
647
  }
630
648
 
@@ -672,6 +690,17 @@ function refresh_node_shape_buttons(gobj, $root)
672
690
  $labels.setAttribute('aria-pressed', labels? 'true' : 'false');
673
691
  $labels.disabled = (mode !== 'shape');
674
692
  }
693
+
694
+ let elbow = (gobj_read_str_attr(gobj, "edge_shape") === "elbow");
695
+ set_pressed_state($root, '.GRAPH_EDGE_ELBOW', elbow);
696
+ let $elbow = $root.querySelector('.GRAPH_EDGE_ELBOW');
697
+ if($elbow) {
698
+ $elbow.setAttribute('aria-pressed', elbow? 'true' : 'false');
699
+ let child = gobj.priv.gobj_nodes_tree;
700
+ let rowless = child? (gobj_read_attr(child, "rowless_layouts") || []) : [];
701
+ let layout = child? gobj_read_str_attr(child, "layout") : "";
702
+ $elbow.disabled = rowless.includes(layout);
703
+ }
675
704
  }
676
705
 
677
706
  /************************************************************
@@ -1319,6 +1348,7 @@ function populate_nodes_tree_options(gobj)
1319
1348
  $layout_select.value = current_layout;
1320
1349
  }
1321
1350
  refresh_layout_stepper(gobj);
1351
+ refresh_node_shape_buttons(gobj, $container);
1322
1352
  }
1323
1353
 
1324
1354
  // Restore persisted operation_mode selection
@@ -2614,6 +2644,7 @@ function ac_set_layout(gobj, event, kw, src)
2614
2644
  gobj
2615
2645
  );
2616
2646
  refresh_layout_stepper(gobj);
2647
+ refresh_node_shape_buttons(gobj, priv.$container);
2617
2648
 
2618
2649
  return 0;
2619
2650
  }
@@ -3116,6 +3147,7 @@ function ac_layout_autoset(gobj, event, kw, src)
3116
3147
  }
3117
3148
  $select.value = layout;
3118
3149
  refresh_layout_stepper(gobj);
3150
+ refresh_node_shape_buttons(gobj, $container);
3119
3151
  return 0;
3120
3152
  }
3121
3153
 
@@ -3213,6 +3245,20 @@ function ac_set_node_mode(gobj, event, kw, src)
3213
3245
  return 0;
3214
3246
  }
3215
3247
 
3248
+ function ac_toggle_edge_shape(gobj, event, kw, src)
3249
+ {
3250
+ let priv = gobj.priv;
3251
+ let shape = (gobj_read_str_attr(gobj, "edge_shape") === "elbow")? "curved" : "elbow";
3252
+
3253
+ gobj_write_str_attr(gobj, "edge_shape", shape);
3254
+ gobj_save_persistent_attrs(gobj, "edge_shape");
3255
+ refresh_node_shape_buttons(gobj, priv.$container);
3256
+ if(priv.gobj_nodes_tree) {
3257
+ gobj_send_event(priv.gobj_nodes_tree, "EV_SET_EDGE_SHAPE", {edge_shape: shape}, gobj);
3258
+ }
3259
+ return 0;
3260
+ }
3261
+
3216
3262
  function ac_toggle_node_labels(gobj, event, kw, src)
3217
3263
  {
3218
3264
  let priv = gobj.priv;
@@ -3395,6 +3441,7 @@ function create_gclass(gclass_name)
3395
3441
  ["EV_COLLAPSE_LEVEL", ac_fold_level_step, null],
3396
3442
  ["EV_SET_NODE_MODE", ac_set_node_mode, null],
3397
3443
  ["EV_TOGGLE_NODE_LABELS", ac_toggle_node_labels, null],
3444
+ ["EV_TOGGLE_EDGE_SHAPE", ac_toggle_edge_shape, null],
3398
3445
  ["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
3399
3446
  ["EV_CAMERA_CHANGED", ac_camera_changed, null],
3400
3447
  ["EV_LEGEND_TOPIC", ac_legend_topic, null],
@@ -3441,6 +3488,7 @@ function create_gclass(gclass_name)
3441
3488
  ["EV_COLLAPSE_LEVEL", 0],
3442
3489
  ["EV_SET_NODE_MODE", 0],
3443
3490
  ["EV_TOGGLE_NODE_LABELS", 0],
3491
+ ["EV_TOGGLE_EDGE_SHAPE", 0],
3444
3492
  ["EV_FIND_RESULT", 0],
3445
3493
  ["EV_LAYOUT_AUTOSET", 0],
3446
3494
  ["EV_CAMERA_CHANGED", 0],
@@ -0,0 +1,102 @@
1
+ /***********************************************************************
2
+ * treedb_elbow.js
3
+ *
4
+ * Where an elbow edge of the treedb graph turns, as plain
5
+ * geometry: the two elbow edge types of c_g6_nodes_tree.js ask
6
+ * this module for their control points on every draw, and it
7
+ * can be tested without G6.
8
+ *
9
+ * An elbow is mxGraph's tree routing: straight out of the
10
+ * parent's port to the channel half way to the child, along it,
11
+ * and straight into the child's port. Two cases are not that:
12
+ *
13
+ * - An edge that does not run FORWARD: the child above its
14
+ * parent, or in the same row. One of the two edges of a
15
+ * reciprocal pair (A -> B and B -> A) is always one. A channel
16
+ * half way between would cross both cards and lie on top of
17
+ * its partner, so it goes ROUND: a short run out of its port,
18
+ * along the outside of both cards, and into the other port
19
+ * from that port's own side.
20
+ *
21
+ * - Several edges joining the SAME two cards: a reciprocal pair,
22
+ * or two hooks of one parent reaching one child. Each gets a
23
+ * LANE: the first stays where it is, the next moves one step to
24
+ * one side, the next one step to the other. The lane follows
25
+ * the order the edges were made, so a new edge never moves the
26
+ * ones already drawn.
27
+ *
28
+ * Written for the top-down reading; left to right is the same
29
+ * geometry with the axes swapped.
30
+ *
31
+ * Copyright (c) 2026, ArtGins.
32
+ * All Rights Reserved.
33
+ ***********************************************************************/
34
+
35
+ export const ELBOW_STEP = 10; /* between two lanes */
36
+ export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
37
+ export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
38
+
39
+ /************************************************************
40
+ * The lane of `id` among `ids`, the edges joining the same two
41
+ * cards: 0, then +1, -1, +2, -2... in the order they were made.
42
+ * Edge ids end in a counter, so a numeric sort is that order.
43
+ ************************************************************/
44
+ export function elbow_lane(ids, id)
45
+ {
46
+ let sorted = ids.slice().sort(
47
+ (a, b) => String(a).localeCompare(String(b), undefined, {numeric: true})
48
+ );
49
+ let k = sorted.indexOf(id);
50
+ if(k <= 0) {
51
+ return 0;
52
+ }
53
+ let side = (k % 2)? 1 : -1;
54
+ return side * Math.ceil(k / 2);
55
+ }
56
+
57
+ function swap(p)
58
+ {
59
+ return [p[1], p[0]];
60
+ }
61
+
62
+ function swap_box(b)
63
+ {
64
+ return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
65
+ }
66
+
67
+ /************************************************************
68
+ * The control points of an elbow from the port `s` to the port
69
+ * `t` ([x, y] each), knowing the boxes of the two cards
70
+ * ({x1, y1, x2, y2}) and the edge's lane.
71
+ *
72
+ * vertical the rows are stacked (top to bottom); false
73
+ * when they are side by side (left to right)
74
+ ************************************************************/
75
+ export function elbow_points(s, t, box_s, box_t, lane, vertical)
76
+ {
77
+ if(!vertical) {
78
+ return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
79
+ .map(swap);
80
+ }
81
+
82
+ let shift = lane * ELBOW_STEP;
83
+
84
+ /* Forward: the channel half way down, moved by the lane and
85
+ * kept strictly between the two ports. */
86
+ if(t[1] - s[1] > 2 * ELBOW_STEP) {
87
+ let mid = (s[1] + t[1]) / 2 + shift;
88
+ mid = Math.min(Math.max(mid, s[1] + ELBOW_STEP), t[1] - ELBOW_STEP);
89
+ return [[s[0], mid], [t[0], mid]];
90
+ }
91
+
92
+ /* Backwards, or across one row: round the outside. A negative
93
+ * lane goes round the left, any other the right, so the two
94
+ * detours of one pair never share a side. */
95
+ let out = ELBOW_JETTY + Math.abs(shift);
96
+ let side = (lane < 0)
97
+ ? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
98
+ : Math.max(box_s.x2, box_t.x2) + ELBOW_CLEAR + Math.abs(shift);
99
+ let y1 = s[1] + out; /* a hook port is on the bottom edge */
100
+ let y2 = t[1] - out; /* an fkey port is on the top edge */
101
+ return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
102
+ }
@@ -0,0 +1,105 @@
1
+ /***********************************************************************
2
+ * treedb_elbow.test.js
3
+ *
4
+ * The elbow edges of the treedb graph, pinned: the lane of an
5
+ * edge among those joining the same two cards, the channel of a
6
+ * forward edge, the detour of one that is not.
7
+ ***********************************************************************/
8
+ import { describe, test, expect } from "vitest";
9
+ import { elbow_lane, elbow_points, ELBOW_STEP, ELBOW_CLEAR } from "./treedb_elbow.js";
10
+
11
+ /* Two cards 100x40: A on top, B one row below it. */
12
+ const A = {x1: 0, y1: 0, x2: 100, y2: 40};
13
+ const B = {x1: 0, y1: 130, x2: 100, y2: 170};
14
+ const A_HOOK = [50, 40]; /* A's bottom edge */
15
+ const A_FKEY = [50, 0]; /* A's top edge */
16
+ const B_HOOK = [60, 170];
17
+ const B_FKEY = [50, 130];
18
+
19
+ describe("the lane", () => {
20
+ test("one edge alone keeps the middle", () => {
21
+ expect(elbow_lane(["edge-t-7"], "edge-t-7")).toBe(0);
22
+ });
23
+
24
+ test("lanes follow the order the edges were made, not the string order", () => {
25
+ let ids = ["edge-t-10", "edge-t-9", "edge-t-11"];
26
+ expect(elbow_lane(ids, "edge-t-9")).toBe(0);
27
+ expect(elbow_lane(ids, "edge-t-10")).toBe(1);
28
+ expect(elbow_lane(ids, "edge-t-11")).toBe(-1);
29
+ });
30
+
31
+ test("a new edge does not move the ones already drawn", () => {
32
+ let before = ["edge-t-1", "edge-t-2"];
33
+ let after = before.concat(["edge-t-3"]);
34
+ for(let id of before) {
35
+ expect(elbow_lane(after, id)).toBe(elbow_lane(before, id));
36
+ }
37
+ });
38
+ });
39
+
40
+ describe("a forward edge", () => {
41
+ test("turns in the channel half way between the ports", () => {
42
+ let pts = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
43
+ expect(pts).toEqual([[50, 85], [50, 85]]);
44
+ });
45
+
46
+ test("its lane moves the channel, and keeps it between the ports", () => {
47
+ let pts = elbow_points(A_HOOK, B_FKEY, A, B, 1, true);
48
+ expect(pts[0][1]).toBe(85 + ELBOW_STEP);
49
+ let far = elbow_points(A_HOOK, B_FKEY, A, B, 20, true);
50
+ expect(far[0][1]).toBeLessThan(B_FKEY[1]);
51
+ expect(far[0][1]).toBeGreaterThan(A_HOOK[1]);
52
+ });
53
+ });
54
+
55
+ describe("an edge that does not run forward", () => {
56
+ test("goes round the outside of both cards", () => {
57
+ /* B -> A: out of B's bottom, back up into A's top. */
58
+ let pts = elbow_points(B_HOOK, A_FKEY, B, A, 0, true);
59
+ expect(pts.length).toBe(4);
60
+ let side = pts[1][0];
61
+ expect(side).toBeGreaterThanOrEqual(100 + ELBOW_CLEAR);
62
+ expect(pts[2][0]).toBe(side);
63
+ expect(pts[0][1]).toBeGreaterThan(B.y2); /* below B */
64
+ expect(pts[3][1]).toBeLessThan(A.y1); /* above A */
65
+ });
66
+
67
+ test("a reciprocal pair shares no segment", () => {
68
+ let forward = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
69
+ let back = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
70
+ let channel = forward[0][1];
71
+ expect(back.every((p) => p[1] !== channel)).toBe(true);
72
+ expect(back[1][0]).toBeGreaterThan(A.x2);
73
+ });
74
+
75
+ test("a negative lane goes round the other side", () => {
76
+ let right = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
77
+ let left = elbow_points(B_HOOK, A_FKEY, B, A, -1, true);
78
+ expect(right[1][0]).toBeGreaterThan(A.x2);
79
+ expect(left[1][0]).toBeLessThan(A.x1);
80
+ });
81
+
82
+ test("two cards in one row are not joined through them", () => {
83
+ /* C beside A, same row: A's hook (bottom) to C's fkey (top). */
84
+ let C = {x1: 200, y1: 0, x2: 300, y2: 40};
85
+ let pts = elbow_points(A_HOOK, [250, 0], A, C, 0, true);
86
+ expect(pts.length).toBe(4);
87
+ expect(pts[1][0]).toBeGreaterThan(C.x2);
88
+ });
89
+ });
90
+
91
+ describe("read left to right", () => {
92
+ test("is the same geometry with the axes swapped", () => {
93
+ let sw = (p) => [p[1], p[0]];
94
+ let swb = (b) => ({x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2});
95
+ for(let [s, t, bs, bt, lane] of [
96
+ [A_HOOK, B_FKEY, A, B, 0],
97
+ [A_HOOK, B_FKEY, A, B, 1],
98
+ [B_HOOK, A_FKEY, B, A, -1],
99
+ ]) {
100
+ let down = elbow_points(s, t, bs, bt, lane, true);
101
+ let right = elbow_points(sw(s), sw(t), swb(bs), swb(bt), lane, false);
102
+ expect(right).toEqual(down.map(sw));
103
+ }
104
+ });
105
+ });
package/src/yui_icons.css CHANGED
@@ -444,3 +444,9 @@
444
444
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zm11.3-387.3l104 104c4.6 4.6 5.9 11.5 3.5 17.4S366.5 256 360 256l-56 0 0 96c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-96-56 0c-6.5 0-12.3-3.9-14.8-9.9s-1.1-12.9 3.5-17.4l104-104c6.2-6.2 16.4-6.2 22.6 0z'/%3E%3C/svg%3E");
445
445
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zm11.3-387.3l104 104c4.6 4.6 5.9 11.5 3.5 17.4S366.5 256 360 256l-56 0 0 96c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-96-56 0c-6.5 0-12.3-3.9-14.8-9.9s-1.1-12.9 3.5-17.4l104-104c6.2-6.2 16.4-6.2 22.6 0z'/%3E%3C/svg%3E");
446
446
  }
447
+
448
+ /* Elbow edges: boxes joined by right-angled lines (FontAwesome Free 7.3.1). */
449
+ .yi-diagram-project::before {
450
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M0 80C0 53.5 21.5 32 48 32l96 0c26.5 0 48 21.5 48 48l0 16 128 0 0-16c0-26.5 21.5-48 48-48l96 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-16-128 0 0 16c0 7.3-1.7 14.3-4.6 20.5l68.6 91.5 80 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-96c0-7.3 1.7-14.3 4.6-20.5L128 224 48 224c-26.5 0-48-21.5-48-48L0 80z'/%3E%3C/svg%3E");
451
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M0 80C0 53.5 21.5 32 48 32l96 0c26.5 0 48 21.5 48 48l0 16 128 0 0-16c0-26.5 21.5-48 48-48l96 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-16-128 0 0 16c0 7.3-1.7 14.3-4.6 20.5l68.6 91.5 80 0c26.5 0 48 21.5 48 48l0 96c0 26.5-21.5 48-48 48l-96 0c-26.5 0-48-21.5-48-48l0-96c0-7.3 1.7-14.3 4.6-20.5L128 224 48 224c-26.5 0-48-21.5-48-48L0 80z'/%3E%3C/svg%3E");
452
+ }