@yuneta/gobj-ui 7.23.29 → 7.23.31

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.
@@ -674,6 +674,30 @@ function make_toolbar(gobj)
674
674
  return $toolbar;
675
675
  }
676
676
 
677
+ /************************************************************
678
+ * The element G6 mounts its canvas in.
679
+ *
680
+ * Read from THIS gclass's own $container, never with
681
+ * `document.getElementById()`: a view is not always in the
682
+ * document when it is started -- a host that builds the
683
+ * viewer and mounts it into a window afterwards starts it
684
+ * DETACHED -- and a global lookup answers null there. It cost
685
+ * a resize that never worked: the ResizeObserver below was
686
+ * simply never attached, silently, and the canvas kept the
687
+ * size it was born with for the life of the window.
688
+ *
689
+ * Null only if the UI was never built; that is a bug, and it
690
+ * is logged where it matters rather than skipped.
691
+ ************************************************************/
692
+ function canvas_mount(gobj)
693
+ {
694
+ let $container = gobj_read_attr(gobj, "$container");
695
+ if(!$container) {
696
+ return null;
697
+ }
698
+ return $container.querySelector("#" + gobj.priv.canvas_id);
699
+ }
700
+
677
701
  /************************************************************
678
702
  * Build a G6 graph instance
679
703
  ************************************************************/
@@ -778,8 +802,10 @@ function build_graph(gobj)
778
802
  * the observed box so there is no feedback loop.
779
803
  */
780
804
  if(typeof ResizeObserver !== "undefined") {
781
- let $canvas = document.getElementById(priv.canvas_id);
782
- if($canvas) {
805
+ let $canvas = canvas_mount(gobj);
806
+ if(!$canvas) {
807
+ log_error(`${gobj_short_name(gobj)}: no canvas mount, the graph will never resize`);
808
+ } else {
783
809
  let ro = new ResizeObserver(() => {
784
810
  if(priv._resize_raf) {
785
811
  cancelAnimationFrame(priv._resize_raf);
@@ -2333,7 +2359,7 @@ function ac_show(gobj, event, kw, src)
2333
2359
  let graph = priv.graph;
2334
2360
 
2335
2361
  if(graph) {
2336
- let $canvas = document.getElementById(priv.canvas_id);
2362
+ let $canvas = canvas_mount(gobj);
2337
2363
  if($canvas) {
2338
2364
  /*
2339
2365
  * Content box (clientWidth/Height), not getBoundingClientRect:
@@ -2366,7 +2392,7 @@ function ac_resize(gobj, event, kw, src)
2366
2392
  let graph = priv.graph;
2367
2393
 
2368
2394
  if(graph) {
2369
- let $canvas = document.getElementById(priv.canvas_id);
2395
+ let $canvas = canvas_mount(gobj);
2370
2396
  if($canvas) {
2371
2397
  let cw = $canvas.clientWidth;
2372
2398
  let ch = $canvas.clientHeight;
@@ -771,7 +771,14 @@ function open_json_viewer(gobj)
771
771
  return;
772
772
  }
773
773
  priv.json_gobj = jv;
774
- gobj_start(jv);
774
+
775
+ /* CREATED, not started. `mt_create` builds the DOM; `mt_start`
776
+ * RENDERS it -- and the viewer's graph view measures a canvas and
777
+ * puts a ResizeObserver on it, both of which need the element to be
778
+ * in the document, which the presenter below is what does. Started
779
+ * here it worked only while the reader had last used the tree view;
780
+ * with the graph view remembered it built detached, attached no
781
+ * observer, and the canvas never followed the window again. */
775
782
  let $box = gobj_read_pointer_attr(jv, "$container");
776
783
 
777
784
  if(mobile) {
@@ -793,6 +800,7 @@ function open_json_viewer(gobj)
793
800
  gobj_send_event(gobj, "EV_JSON_CLOSED", {}, gobj);
794
801
  }
795
802
  });
803
+ gobj_start(jv); /* mounted: now it can measure itself */
796
804
  } else {
797
805
  let $win_parent = (shell && yui_shell_popup_layer(shell)) ||
798
806
  (typeof document !== "undefined" && document.getElementById("top-layer")) ||
@@ -833,6 +841,7 @@ function open_json_viewer(gobj)
833
841
  return;
834
842
  }
835
843
  gobj_start(priv.json_win);
844
+ gobj_start(jv); /* mounted: now it can measure itself */
836
845
  }
837
846
 
838
847
  request_print_tranger(gobj, ""); /* first fetch: whole tranger, collapsed */
@@ -877,11 +886,16 @@ function close_json_viewer(gobj)
877
886
  }
878
887
  if(jv && is_gobj(jv)) {
879
888
  try {
880
- /* STOP, then destroy — the viewer was STARTED in open_json_viewer.
881
- * gobj_destroy() raises the `destroying` flag before it can stop a
882
- * running gobj, so destroying it straight logs "Destroying a
883
- * RUNNING gobj" + "gobj NULL or DESTROYED" and skips its mt_stop. */
884
- gobj_stop(jv);
889
+ /* STOP, then destroy: gobj_destroy() raises the `destroying`
890
+ * flag before it can stop a running gobj, so destroying it
891
+ * straight logs "Destroying a RUNNING gobj" + "gobj NULL or
892
+ * DESTROYED" and skips its mt_stop. GUARDED, because the
893
+ * viewer is now started AFTER its presenter is up: the two
894
+ * failure paths that get here arrive with it created and
895
+ * never started. */
896
+ if(gobj_is_running(jv)) {
897
+ gobj_stop(jv);
898
+ }
885
899
  gobj_destroy(jv);
886
900
  } catch(e) {
887
901
  log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
@@ -0,0 +1,234 @@
1
+ /***********************************************************************
2
+ * gclass_describe.js
3
+ *
4
+ * A gclass, as plain JSON, in the shape the BACKEND answers.
5
+ *
6
+ * There is no gclass viewer in the framework: the C kernel
7
+ * answers `view-gclass` with `gclass2json()`
8
+ * (`kernel/c/gobj-c/src/gobj.c`) and nothing draws it. This
9
+ * module builds the same document for a gclass of the BROWSER
10
+ * yuno, out of the gobj-js registry, so one viewer can show a
11
+ * gclass of either side:
12
+ *
13
+ * {id, gcflag, attrs, commands, gclass_methods,
14
+ * internal_methods, FSM: {events, states}}
15
+ *
16
+ * Two keys the backend does not carry are added at the end,
17
+ * because the browser can answer them for free and the reader
18
+ * asks them first: `instances` and `trace_levels`.
19
+ *
20
+ * Copyright (c) 2026, ArtGins.
21
+ * All Rights Reserved.
22
+ ***********************************************************************/
23
+ import {
24
+ data_type_t,
25
+ sdata_flag_t,
26
+ event_flag_t,
27
+ gclass_flag_t,
28
+ gclass_find_by_name,
29
+ } from "@yuneta/gobj-js";
30
+
31
+
32
+ /***************************************************************
33
+ * Bit tables
34
+ ***************************************************************/
35
+ const DATA_TYPE_NAMES = {
36
+ [data_type_t.DTP_STRING]: "DTP_STRING",
37
+ [data_type_t.DTP_BOOLEAN]: "DTP_BOOLEAN",
38
+ [data_type_t.DTP_INTEGER]: "DTP_INTEGER",
39
+ [data_type_t.DTP_REAL]: "DTP_REAL",
40
+ [data_type_t.DTP_LIST]: "DTP_LIST",
41
+ [data_type_t.DTP_DICT]: "DTP_DICT",
42
+ [data_type_t.DTP_JSON]: "DTP_JSON",
43
+ [data_type_t.DTP_POINTER]: "DTP_POINTER",
44
+ };
45
+
46
+ /************************************************************
47
+ * The names of the bits set in `bits`, in table order.
48
+ ************************************************************/
49
+ function bits2names(table, bits)
50
+ {
51
+ let names = [];
52
+ let value = Number(bits) || 0;
53
+
54
+ for(let name of Object.keys(table)) {
55
+ if((value & table[name]) !== 0) {
56
+ names.push(name);
57
+ }
58
+ }
59
+ return names;
60
+ }
61
+
62
+ /************************************************************
63
+ * The name of a data type. An unknown type is reported as
64
+ * the number it is -- silently drawing "DTP_STRING" for a
65
+ * type nobody registered would be worse than saying "17".
66
+ ************************************************************/
67
+ function data_type_name(type)
68
+ {
69
+ return DATA_TYPE_NAMES[type] || String(type);
70
+ }
71
+
72
+ /************************************************************
73
+ * One SDATA descriptor -> one row.
74
+ *
75
+ * A DTP_POINTER default is a live object (a DOM node, a
76
+ * gobj): it is never printed, only named, because a viewer
77
+ * that serializes it would either throw or dump the page.
78
+ ************************************************************/
79
+ function sdatadesc2json(it)
80
+ {
81
+ let row = {
82
+ id: it.name,
83
+ type: data_type_name(it.type),
84
+ flag: bits2names(sdata_flag_t, it.flag),
85
+ description: it.description || "",
86
+ };
87
+
88
+ if(it.type === data_type_t.DTP_POINTER) {
89
+ row.default_value = (it.default_value === null ||
90
+ it.default_value === undefined)? null: "<pointer>";
91
+ } else {
92
+ row.default_value = (it.default_value === undefined)?
93
+ null: it.default_value;
94
+ }
95
+
96
+ return row;
97
+ }
98
+
99
+ /************************************************************
100
+ * The attrs table of a gclass.
101
+ ************************************************************/
102
+ function attrs2json(attrs_table)
103
+ {
104
+ let rows = [];
105
+ if(!Array.isArray(attrs_table)) {
106
+ return rows;
107
+ }
108
+ for(let it of attrs_table) {
109
+ if(!it || !it.name) {
110
+ continue; /* SDATA_END() */
111
+ }
112
+ rows.push(sdatadesc2json(it));
113
+ }
114
+ return rows;
115
+ }
116
+
117
+ /************************************************************
118
+ * The command table of a gclass, with the parameters of
119
+ * each command (the `schema` slot of SDATACM).
120
+ ************************************************************/
121
+ function commands2json(command_table)
122
+ {
123
+ let rows = [];
124
+ if(!Array.isArray(command_table)) {
125
+ return rows;
126
+ }
127
+ for(let it of command_table) {
128
+ if(!it || !it.name) {
129
+ continue; /* SDATA_END() */
130
+ }
131
+ let row = {
132
+ id: it.name,
133
+ alias: it.alias || [],
134
+ description: it.description || "",
135
+ };
136
+ if(Array.isArray(it.schema)) {
137
+ row.parameters = attrs2json(it.schema);
138
+ }
139
+ rows.push(row);
140
+ }
141
+ return rows;
142
+ }
143
+
144
+ /************************************************************
145
+ * The methods the gclass actually implements. A table of
146
+ * every possible one, mostly empty, tells the reader nothing.
147
+ ************************************************************/
148
+ function methods2json(mt)
149
+ {
150
+ let names = [];
151
+ if(!mt || typeof mt !== "object") {
152
+ return names;
153
+ }
154
+ for(let key of Object.keys(mt)) {
155
+ if(typeof mt[key] === "function") {
156
+ names.push(key);
157
+ }
158
+ }
159
+ return names;
160
+ }
161
+
162
+ /************************************************************
163
+ * The FSM: the events the gclass declares, and what each
164
+ * state does with them.
165
+ ************************************************************/
166
+ function fsm2json(gclass)
167
+ {
168
+ let events = [];
169
+ for(let ev of (gclass.dl_events || [])) {
170
+ events.push({
171
+ event_name: ev.event_name,
172
+ event_flag: bits2names(event_flag_t, ev.event_flag),
173
+ });
174
+ }
175
+
176
+ let states = {};
177
+ for(let st of (gclass.dl_states || [])) {
178
+ let actions = [];
179
+ for(let ac of (st.dl_actions || [])) {
180
+ actions.push({
181
+ event_name: ac.event_name,
182
+ action: ac.action? (ac.action.name || "action"): "",
183
+ next_state: ac.next_state || "",
184
+ });
185
+ }
186
+ states[st.state_name] = actions;
187
+ }
188
+
189
+ return {events: events, states: states};
190
+ }
191
+
192
+ /************************************************************
193
+ * The description of a gclass REGISTERED IN THIS BROWSER YUNO,
194
+ * or null when the registry does not know that name.
195
+ *
196
+ * Null is the answer for a gclass of a BACKEND yuno too: the
197
+ * browser registry carries the GUI gclasses and nothing else,
198
+ * so the caller asks the backend `view-gclass` for those and
199
+ * shows whatever it answers -- this same shape.
200
+ ************************************************************/
201
+ function describe_local_gclass(gclass_name)
202
+ {
203
+ if(!gclass_name) {
204
+ return null;
205
+ }
206
+
207
+ let gclass = gclass_find_by_name(gclass_name, false);
208
+ if(!gclass) {
209
+ return null;
210
+ }
211
+
212
+ return {
213
+ id: gclass.gclass_name,
214
+ gcflag: bits2names(gclass_flag_t, gclass.gclass_flag),
215
+ attrs: attrs2json(gclass.attrs_table),
216
+ commands: commands2json(gclass.command_table),
217
+ gclass_methods: methods2json(gclass.gmt),
218
+ internal_methods: methods2json(gclass.lmt),
219
+ FSM: fsm2json(gclass),
220
+ instances: Number(gclass.instances) || 0,
221
+ trace_levels: Array.isArray(gclass.s_user_trace_level)?
222
+ gclass.s_user_trace_level: [],
223
+ };
224
+ }
225
+
226
+ export {
227
+ bits2names,
228
+ data_type_name,
229
+ attrs2json,
230
+ commands2json,
231
+ methods2json,
232
+ fsm2json,
233
+ describe_local_gclass,
234
+ };
@@ -0,0 +1,125 @@
1
+ import {describe, it, expect, beforeAll} from "vitest";
2
+ import {
3
+ SDATA,
4
+ SDATA_END,
5
+ SDATACM,
6
+ data_type_t,
7
+ event_flag_t,
8
+ gclass_create,
9
+ gclass_flag_t,
10
+ sdata_flag_t,
11
+ } from "@yuneta/gobj-js";
12
+
13
+ import {bits2names, describe_local_gclass} from "./gclass_describe.js";
14
+
15
+ /* A gclass with one of everything the viewer draws. */
16
+ beforeAll(() => {
17
+ const attrs_table = [
18
+ SDATA(data_type_t.DTP_STRING, "title", sdata_flag_t.SDF_RD, "", "The title"),
19
+ SDATA(data_type_t.DTP_INTEGER, "timeout",
20
+ sdata_flag_t.SDF_WR|sdata_flag_t.SDF_PERSIST, 1000, "Timeout"),
21
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "The DOM node"),
22
+ SDATA_END(),
23
+ ];
24
+ const command_table = [
25
+ SDATACM(data_type_t.DTP_STRING, "help", ["h"], [
26
+ SDATA(data_type_t.DTP_STRING, "cmd", 0, "", "The command"),
27
+ SDATA_END(),
28
+ ], null, "Ask for help"),
29
+ SDATA_END(),
30
+ ];
31
+
32
+ gclass_create(
33
+ "C_TEST_DESCRIBE",
34
+ [
35
+ ["EV_GO", 0],
36
+ ["EV_DONE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
37
+ ],
38
+ [
39
+ ["ST_IDLE", [["EV_GO", function ac_go() {}, "ST_WORKING"]]],
40
+ ["ST_WORKING", [["EV_DONE", function ac_done() {}, "ST_IDLE"]]],
41
+ ],
42
+ {mt_create: function() {}, mt_start: function() {}},
43
+ 0,
44
+ attrs_table,
45
+ {},
46
+ 0,
47
+ command_table,
48
+ 0,
49
+ gclass_flag_t.gcflag_singleton
50
+ );
51
+ });
52
+
53
+ describe("bits2names", () => {
54
+ it("names every bit set, and nothing else", () => {
55
+ expect(bits2names(sdata_flag_t,
56
+ sdata_flag_t.SDF_RD|sdata_flag_t.SDF_PERSIST))
57
+ .toEqual(["SDF_RD", "SDF_PERSIST"]);
58
+ expect(bits2names(sdata_flag_t, 0)).toEqual([]);
59
+ });
60
+ });
61
+
62
+ describe("describe_local_gclass", () => {
63
+ it("an unregistered gclass is null, never an empty document", () => {
64
+ expect(describe_local_gclass("C_NOT_THERE")).toBe(null);
65
+ expect(describe_local_gclass("")).toBe(null);
66
+ });
67
+
68
+ it("answers the shape the backend view-gclass answers", () => {
69
+ let g = describe_local_gclass("C_TEST_DESCRIBE");
70
+ expect(Object.keys(g)).toEqual([
71
+ "id", "gcflag", "attrs", "commands", "gclass_methods",
72
+ "internal_methods", "FSM", "instances", "trace_levels",
73
+ ]);
74
+ expect(g.id).toBe("C_TEST_DESCRIBE");
75
+ expect(g.gcflag).toEqual(["gcflag_singleton"]);
76
+ });
77
+
78
+ it("names the type and the flags of every attr", () => {
79
+ let attrs = describe_local_gclass("C_TEST_DESCRIBE").attrs;
80
+ expect(attrs.length).toBe(3);
81
+ expect(attrs[0]).toEqual({
82
+ id: "title",
83
+ type: "DTP_STRING",
84
+ flag: ["SDF_RD"],
85
+ description: "The title",
86
+ default_value: "",
87
+ });
88
+ expect(attrs[1].flag).toEqual(["SDF_WR", "SDF_PERSIST"]);
89
+ expect(attrs[1].default_value).toBe(1000);
90
+ });
91
+
92
+ it("never prints a pointer default -- it names it", () => {
93
+ let attrs = describe_local_gclass("C_TEST_DESCRIBE").attrs;
94
+ let container = attrs.find(a => a.id === "$container");
95
+ expect(container.type).toBe("DTP_POINTER");
96
+ expect(container.default_value).toBe(null);
97
+ });
98
+
99
+ it("carries the commands with their parameters", () => {
100
+ let cmds = describe_local_gclass("C_TEST_DESCRIBE").commands;
101
+ expect(cmds.length).toBe(1);
102
+ expect(cmds[0].id).toBe("help");
103
+ expect(cmds[0].alias).toEqual(["h"]);
104
+ expect(cmds[0].parameters.map(p => p.id)).toEqual(["cmd"]);
105
+ });
106
+
107
+ it("lists only the methods the gclass implements", () => {
108
+ let g = describe_local_gclass("C_TEST_DESCRIBE");
109
+ expect(g.gclass_methods).toEqual(["mt_create", "mt_start"]);
110
+ expect(g.internal_methods).toEqual([]);
111
+ });
112
+
113
+ it("draws the FSM as events plus what each state does with them", () => {
114
+ let fsm = describe_local_gclass("C_TEST_DESCRIBE").FSM;
115
+ expect(fsm.events).toEqual([
116
+ {event_name: "EV_GO", event_flag: []},
117
+ {event_name: "EV_DONE",
118
+ event_flag: ["EVF_NO_WARN_SUBS", "EVF_OUTPUT_EVENT"]},
119
+ ]);
120
+ expect(Object.keys(fsm.states)).toEqual(["ST_IDLE", "ST_WORKING"]);
121
+ expect(fsm.states["ST_IDLE"]).toEqual([
122
+ {event_name: "EV_GO", action: "ac_go", next_state: "ST_WORKING"},
123
+ ]);
124
+ });
125
+ });