@yuneta/gobj-ui 7.23.36 → 7.23.38

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
@@ -1433,6 +1433,11 @@ description describes the CLASS and cannot carry it, so the host supplies it —
1433
1433
  the gobj tree passes `node_data.state` — and the machine lights that column.
1434
1434
  Omitted, nothing is lit.
1435
1435
 
1436
+ `close_gclass_view(handle)` is the host-driven teardown, and the ✕ runs it too:
1437
+ the window destroys itself, but the viewer hangs from the HOST, so nothing else
1438
+ would take it down — and a viewer left alive keeps its service name, which made
1439
+ the next click answer *"service ALREADY registered"* (fixed in 7.23.38).
1440
+
1436
1441
  `gclass_view_available()` registers `C_YUI_GCLASS` on demand and reports
1437
1442
  whether it is there, so no app has to mount a gclass for a control it never
1438
1443
  asked for. The gobj tree still asks before drawing its `gclass` button: a
@@ -1467,7 +1472,10 @@ for the other side:
1467
1472
 
1468
1473
  The last one is not a rename: `states2json()` cannot name an action, so a cell
1469
1474
  from a **backend** gclass says THAT there is one and never which. The viewer
1470
- draws a mark rather than printing a name nobody wrote.
1475
+ draws a mark rather than printing a name nobody wrote — and an em dash for the
1476
+ third case, an event declared with NO action (`{EV_TX_READY, 0, 0}`, which
1477
+ C_WEBSOCKET has in all four states): legal in that state, and deliberately
1478
+ doing nothing.
1471
1479
 
1472
1480
  **The machine is a matrix**, rows = events and columns = states — the shape the
1473
1481
  FSM is declared in, and the one that survives 12 events against 3 states as
@@ -1507,7 +1515,11 @@ What it refuses to draw is the interesting half:
1507
1515
  - **One edge per PAIR of states**, not per transition, labelled with the events
1508
1516
  that make the jump (three, then a count). G6 draws overlapping edges on top
1509
1517
  of each other, so a second edge between the same two states is invisible and
1510
- nothing tells the reader it is there.
1518
+ nothing tells the reader it is there. A RECIPROCAL pair (`A -> B` and
1519
+ `B -> A`) is bowed apart into two arcs — with the SAME `curveOffset` on both,
1520
+ because G6 measures it from each edge's own direction, and with the type
1521
+ varied through a FUNCTION on `edge.type`, because a `type` written on the
1522
+ datum is overridden by the graph-level default.
1511
1523
  - **An event a state handles without leaving is not an edge.** Ten self-loops
1512
1524
  on one card say nothing a number cannot say better, so the card carries
1513
1525
  `n events · m leave` instead.
@@ -14010,7 +14010,7 @@ function build_graph$3(gobj) {
14010
14010
  zoomRange: [.2, 3],
14011
14011
  padding: 24,
14012
14012
  edge: {
14013
- type: "polyline",
14013
+ type: (d) => d && d.data && d.data.reciprocal ? "quadratic" : "polyline",
14014
14014
  style: {
14015
14015
  stroke: edge_color(),
14016
14016
  lineWidth: 1.2,
@@ -14151,11 +14151,18 @@ function build_data(gobj) {
14151
14151
  let shown = pair.events.slice(0, MAX_EDGE_LABELS);
14152
14152
  let label = shown.join("\n");
14153
14153
  if (pair.events.length > shown.length) label += "\n+" + (pair.events.length - shown.length);
14154
+ let style = { labelText: label };
14155
+ let reciprocal = by_pair.has(pair.target + " " + pair.source);
14156
+ if (reciprocal) {
14157
+ style.curveOffset = 34;
14158
+ style.labelPlacement = .5;
14159
+ }
14154
14160
  edges.push({
14155
14161
  id: "e_" + edges.length,
14156
14162
  source: pair.source,
14157
14163
  target: pair.target,
14158
- style: { labelText: label }
14164
+ data: { reciprocal },
14165
+ style
14159
14166
  });
14160
14167
  }
14161
14168
  return {
@@ -15542,7 +15549,7 @@ function build_machine(gobj, fsm) {
15542
15549
  { class: "GCLASS_MX_ACTION" },
15543
15550
  cell.action
15544
15551
  ]);
15545
- else items.push([
15552
+ else if (cell.has_action) items.push([
15546
15553
  "span",
15547
15554
  {
15548
15555
  class: "GCLASS_MX_ACTION is-unnamed",
@@ -15551,6 +15558,15 @@ function build_machine(gobj, fsm) {
15551
15558
  },
15552
15559
  "●"
15553
15560
  ]);
15561
+ else items.push([
15562
+ "span",
15563
+ {
15564
+ class: "GCLASS_MX_ACTION is-none-action",
15565
+ title: (0, i18next.t)("accepted with no action"),
15566
+ "data-i18n-title": "accepted with no action"
15567
+ },
15568
+ "—"
15569
+ ]);
15554
15570
  if (cell.next_state) {
15555
15571
  items.push([
15556
15572
  "span",
@@ -15679,6 +15695,19 @@ function build_legend(fsm, current) {
15679
15695
  (0, i18next.t)("not declared in this state")
15680
15696
  ]]
15681
15697
  ]);
15698
+ if (fsm.rows.some((row) => row.cells.some((cell) => cell && !cell.has_action))) items.push([
15699
+ "span",
15700
+ { class: "GCLASS_LEGEND_ITEM" },
15701
+ [[
15702
+ "i",
15703
+ { class: "GCLASS_LEGEND_SWATCH is-none-action" },
15704
+ "—"
15705
+ ], [
15706
+ "span",
15707
+ { i18n: "accepted with no action" },
15708
+ (0, i18next.t)("accepted with no action")
15709
+ ]]
15710
+ ]);
15682
15711
  if (current && fsm.states.some((s) => s.name === current)) items.push([
15683
15712
  "span",
15684
15713
  { class: "GCLASS_LEGEND_ITEM" },
@@ -16529,6 +16558,9 @@ function open_gclass_view(host, gclass_name, opts) {
16529
16558
  };
16530
16559
  let $box = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(jv, "$container");
16531
16560
  let on_close = () => {
16561
+ handle.win = null;
16562
+ handle.modal = null;
16563
+ close_gclass_view(handle);
16532
16564
  if (typeof opts.on_close === "function") opts.on_close();
16533
16565
  };
16534
16566
  if (is_mobile$3()) {
@@ -14005,7 +14005,7 @@ function build_graph$3(gobj) {
14005
14005
  zoomRange: [.2, 3],
14006
14006
  padding: 24,
14007
14007
  edge: {
14008
- type: "polyline",
14008
+ type: (d) => d && d.data && d.data.reciprocal ? "quadratic" : "polyline",
14009
14009
  style: {
14010
14010
  stroke: edge_color(),
14011
14011
  lineWidth: 1.2,
@@ -14146,11 +14146,18 @@ function build_data(gobj) {
14146
14146
  let shown = pair.events.slice(0, MAX_EDGE_LABELS);
14147
14147
  let label = shown.join("\n");
14148
14148
  if (pair.events.length > shown.length) label += "\n+" + (pair.events.length - shown.length);
14149
+ let style = { labelText: label };
14150
+ let reciprocal = by_pair.has(pair.target + " " + pair.source);
14151
+ if (reciprocal) {
14152
+ style.curveOffset = 34;
14153
+ style.labelPlacement = .5;
14154
+ }
14149
14155
  edges.push({
14150
14156
  id: "e_" + edges.length,
14151
14157
  source: pair.source,
14152
14158
  target: pair.target,
14153
- style: { labelText: label }
14159
+ data: { reciprocal },
14160
+ style
14154
14161
  });
14155
14162
  }
14156
14163
  return {
@@ -15537,7 +15544,7 @@ function build_machine(gobj, fsm) {
15537
15544
  { class: "GCLASS_MX_ACTION" },
15538
15545
  cell.action
15539
15546
  ]);
15540
- else items.push([
15547
+ else if (cell.has_action) items.push([
15541
15548
  "span",
15542
15549
  {
15543
15550
  class: "GCLASS_MX_ACTION is-unnamed",
@@ -15546,6 +15553,15 @@ function build_machine(gobj, fsm) {
15546
15553
  },
15547
15554
  "●"
15548
15555
  ]);
15556
+ else items.push([
15557
+ "span",
15558
+ {
15559
+ class: "GCLASS_MX_ACTION is-none-action",
15560
+ title: t("accepted with no action"),
15561
+ "data-i18n-title": "accepted with no action"
15562
+ },
15563
+ "—"
15564
+ ]);
15549
15565
  if (cell.next_state) {
15550
15566
  items.push([
15551
15567
  "span",
@@ -15674,6 +15690,19 @@ function build_legend(fsm, current) {
15674
15690
  t("not declared in this state")
15675
15691
  ]]
15676
15692
  ]);
15693
+ if (fsm.rows.some((row) => row.cells.some((cell) => cell && !cell.has_action))) items.push([
15694
+ "span",
15695
+ { class: "GCLASS_LEGEND_ITEM" },
15696
+ [[
15697
+ "i",
15698
+ { class: "GCLASS_LEGEND_SWATCH is-none-action" },
15699
+ "—"
15700
+ ], [
15701
+ "span",
15702
+ { i18n: "accepted with no action" },
15703
+ t("accepted with no action")
15704
+ ]]
15705
+ ]);
15677
15706
  if (current && fsm.states.some((s) => s.name === current)) items.push([
15678
15707
  "span",
15679
15708
  { class: "GCLASS_LEGEND_ITEM" },
@@ -16524,6 +16553,9 @@ function open_gclass_view(host, gclass_name, opts) {
16524
16553
  };
16525
16554
  let $box = gobj_read_pointer_attr(jv, "$container");
16526
16555
  let on_close = () => {
16556
+ handle.win = null;
16557
+ handle.modal = null;
16558
+ close_gclass_view(handle);
16527
16559
  if (typeof opts.on_close === "function") opts.on_close();
16528
16560
  };
16529
16561
  if (is_mobile$3()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.36",
3
+ "version": "7.23.38",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -395,7 +395,15 @@ function build_graph(gobj)
395
395
  * the edge of the box. */
396
396
  padding: 24,
397
397
  edge: {
398
- type: 'polyline',
398
+ /*
399
+ * A FUNCTION, not a name: a `type` written on the edge
400
+ * datum is overridden by the graph-level default, so a
401
+ * reciprocal pair went on drawing as two straight
402
+ * segments on top of each other. Asking the datum here is
403
+ * what G6 documents for varying the type per element.
404
+ */
405
+ type: (d) => (d && d.data && d.data.reciprocal)?
406
+ 'quadratic': 'polyline',
399
407
  style: {
400
408
  stroke: edge_color(),
401
409
  lineWidth: 1.2,
@@ -609,11 +617,51 @@ function build_data(gobj)
609
617
  if(pair.events.length > shown.length) {
610
618
  label += "\n+" + (pair.events.length - shown.length);
611
619
  }
620
+
621
+ let style = {labelText: label};
622
+
623
+ /*
624
+ * A pair that goes BOTH ways is drawn as two arcs, one bowing
625
+ * each way.
626
+ *
627
+ * Grouping by pair fixed the transitions that share an ordered
628
+ * pair; a RECIPROCAL pair is a different key and lands on the
629
+ * same straight segment -- so the two arrows overlap and the
630
+ * second label is painted under the first. Real case:
631
+ * C_WEBSOCKET goes to ST_WAIT_HANDSHAKE on EV_CONNECTED and
632
+ * comes back on EV_DISCONNECTED, and only the return was
633
+ * readable.
634
+ */
635
+ let reciprocal = by_pair.has(pair.target + " " + pair.source);
636
+ if(reciprocal) {
637
+ /* Bowed opposite ways so the two arrows separate -- and
638
+ * the two LABELS moved off the midpoint as well, one to
639
+ * each third of its own curve. Bowing alone was not
640
+ * enough: both labels are placed at the middle by default,
641
+ * they carry an opaque background, and the one drawn last
642
+ * simply covered the other. */
643
+ /* The SAME offset on both, not opposite ones. G6 measures
644
+ * `curveOffset` from the edge's OWN direction, and a
645
+ * reciprocal pair runs in opposite directions -- so equal
646
+ * signs bow them to opposite sides of the screen, and
647
+ * opposite signs put them on the same side, one on top of
648
+ * the other.
649
+ *
650
+ * The label stays at the MIDDLE of its arc, where the two
651
+ * are furthest apart. Moving it along the curve instead
652
+ * (0.3 / 0.7) pushed it under a state card: G6's html
653
+ * nodes are DOM elements ABOVE the canvas, so whatever the
654
+ * curve draws under one is invisible. */
655
+ style.curveOffset = 34;
656
+ style.labelPlacement = 0.5;
657
+ }
658
+
612
659
  edges.push({
613
660
  id: "e_" + edges.length,
614
661
  source: pair.source,
615
662
  target: pair.target,
616
- style: {labelText: label},
663
+ data: {reciprocal: reciprocal},
664
+ style: style,
617
665
  });
618
666
  }
619
667
 
@@ -506,3 +506,19 @@
506
506
  min-height: 16rem;
507
507
  max-height: 70vh;
508
508
  }
509
+
510
+ /*
511
+ * An event declared with no action at all: legal here, and it does
512
+ * nothing. Quieter than an action and louder than an empty cell,
513
+ * because it is neither.
514
+ */
515
+ .C_YUI_GCLASS .GCLASS_MX_ACTION.is-none-action {
516
+ color: var(--gclass-muted);
517
+ cursor: help;
518
+ }
519
+
520
+ .C_YUI_GCLASS .GCLASS_LEGEND_SWATCH.is-none-action {
521
+ border-color: transparent;
522
+ color: var(--gclass-muted);
523
+ width: auto;
524
+ }
@@ -910,13 +910,24 @@ function build_machine(gobj, fsm)
910
910
  let items = [];
911
911
  if(cell.action) {
912
912
  items.push(['span', {class: 'GCLASS_MX_ACTION'}, cell.action]);
913
- } else {
913
+ } else if(cell.has_action) {
914
914
  /* The C side cannot name an action: `states2json()`
915
915
  * writes the literal "action" for all of them. A mark
916
916
  * is the honest drawing of "there is one". */
917
917
  items.push(['span', {class: 'GCLASS_MX_ACTION is-unnamed',
918
918
  title: t("action"),
919
919
  'data-i18n-title': 'action'}, "●"]);
920
+ } else {
921
+ /* Declared with NO action and no next state --
922
+ * `{EV_TX_READY, 0, 0}`, four of them in C_WEBSOCKET.
923
+ * It is not the same as an empty cell (which throws)
924
+ * and it is not an action either: the event is legal
925
+ * here and deliberately does nothing. Drawing it as a
926
+ * mark claimed an action the gclass never declared. */
927
+ items.push(['span', {class: 'GCLASS_MX_ACTION is-none-action',
928
+ title: t("accepted with no action"),
929
+ 'data-i18n-title': 'accepted with no action'},
930
+ "—"]);
920
931
  }
921
932
  if(cell.next_state) {
922
933
  items.push(['span', {class: 'GCLASS_MX_ARROW'}, "▸"]);
@@ -1004,6 +1015,19 @@ function build_legend(fsm, current)
1004
1015
  ['span', {i18n: 'not declared in this state'}, t("not declared in this state")]
1005
1016
  ]]);
1006
1017
 
1018
+ /* Only if there is one on screen: a legend for a mark nobody drew
1019
+ * is noise, and most gclasses declare no actionless event. */
1020
+ let has_actionless = fsm.rows.some(
1021
+ (row) => row.cells.some((cell) => cell && !cell.has_action)
1022
+ );
1023
+ if(has_actionless) {
1024
+ items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
1025
+ ['i', {class: 'GCLASS_LEGEND_SWATCH is-none-action'}, "—"],
1026
+ ['span', {i18n: 'accepted with no action'},
1027
+ t("accepted with no action")]
1028
+ ]]);
1029
+ }
1030
+
1007
1031
  if(current && fsm.states.some((s) => s.name === current)) {
1008
1032
  items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
1009
1033
  ['i', {class: 'GCLASS_LEGEND_SWATCH is-current'}],
@@ -215,6 +215,20 @@ describe("build_fsm", () => {
215
215
  expect(fsm.published[0].flags).toEqual(["EVF_OUTPUT_EVENT"]);
216
216
  });
217
217
 
218
+ it("tells a declared-but-actionless cell from an action", () => {
219
+ /* `{EV_TX_READY, 0, 0}` -- four of them in C_WEBSOCKET: the
220
+ * event is legal in that state and deliberately does nothing. */
221
+ const none = build_fsm({
222
+ events: [{event_name: "EV_TX_READY", event_flag: ""}],
223
+ states: {"ST_IDLE": [
224
+ {event_name: "EV_TX_READY", action: "", next_state: ""},
225
+ ]},
226
+ });
227
+ expect(none.rows[0].cells[0]).toEqual({
228
+ action: "", has_action: false, next_state: "",
229
+ });
230
+ });
231
+
218
232
  it("says an action exists without inventing its name", () => {
219
233
  expect(fsm.rows[0].cells[1]).toEqual({
220
234
  action: "", has_action: true, next_state: "",
@@ -158,7 +158,30 @@ function open_gclass_view(host, gclass_name, opts)
158
158
  let handle = {json_gobj: jv, win: null, modal: null};
159
159
  let $box = gobj_read_pointer_attr(jv, "$container");
160
160
 
161
+ /*
162
+ * The ✕ path, and the ONLY one that used to leave the viewer
163
+ * behind. `close_window()` calls this and THEN destroys the
164
+ * window -- but the viewer is not the window's child (it hangs
165
+ * from the host, so it can outlive a window that is only its
166
+ * frame), so nothing else took it down. Alive, it did two things:
167
+ *
168
+ * - it kept its service NAME, so the very next click on the
169
+ * `gclass` button answered "service ALREADY registered:
170
+ * C_YUI_GCLASS^gclass-view-<host>" and no second gclass could
171
+ * be opened for the rest of the session.
172
+ * - it was still RUNNING when the host's own window closed, and
173
+ * the framework logged "Destroying a RUNNING gobj" while
174
+ * taking it down with its parent.
175
+ *
176
+ * The window and the sheet are cleared FIRST: both are already
177
+ * tearing themselves down, and close_gclass_view() must not go
178
+ * back at them.
179
+ */
161
180
  let on_close = () => {
181
+ handle.win = null;
182
+ handle.modal = null;
183
+ close_gclass_view(handle);
184
+
162
185
  if(typeof opts.on_close === "function") {
163
186
  opts.on_close();
164
187
  }