@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.
@@ -0,0 +1,290 @@
1
+ /***********************************************************************
2
+ * yui_gclass_view.js
3
+ *
4
+ * The gclass viewer: what a GClass IS, in a window.
5
+ *
6
+ * The framework had no such viewer. The C kernel answers
7
+ * `view-gclass` with a full description (attrs, commands,
8
+ * methods, FSM) and the only reader of it was a terminal.
9
+ * This one draws that same document with C_YUI_JSON, so the
10
+ * FSM of a gclass is a graph and its attrs table is a tree,
11
+ * and it takes the document from either side:
12
+ *
13
+ * - a gclass of the browser yuno -> describe_local_gclass()
14
+ * - a gclass of a backend yuno -> `opts.description`, the
15
+ * answer of `view-gclass`
16
+ *
17
+ * One window at a time per host: the caller keeps the handle
18
+ * and closes it, the way the treedb graph keeps its raw-JSON
19
+ * viewer.
20
+ *
21
+ * Copyright (c) 2026, ArtGins.
22
+ * All Rights Reserved.
23
+ ***********************************************************************/
24
+ import {
25
+ clean_name,
26
+ gclass_find_by_name,
27
+ gobj_create_service,
28
+ gobj_destroy,
29
+ gobj_find_service,
30
+ gobj_is_running,
31
+ gobj_name,
32
+ gobj_read_pointer_attr,
33
+ gobj_short_name,
34
+ gobj_start,
35
+ gobj_stop,
36
+ is_gobj,
37
+ log_error,
38
+ log_warning,
39
+ } from "@yuneta/gobj-js";
40
+
41
+ import {describe_local_gclass} from "./gclass_describe.js";
42
+ import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
43
+ import {yui_shell_of} from "./c_yui_shell.js";
44
+
45
+ import {t} from "i18next";
46
+
47
+
48
+ /************************************************************
49
+ * Can this app show a gclass at all?
50
+ *
51
+ * The viewer is C_YUI_JSON, and an app registers only the
52
+ * gclasses it mounts. The caller asks this BEFORE drawing a
53
+ * button, so an app that does not carry the viewer shows no
54
+ * control instead of a control that fails.
55
+ ************************************************************/
56
+ function gclass_view_available()
57
+ {
58
+ return !!gclass_find_by_name("C_YUI_JSON", false);
59
+ }
60
+
61
+
62
+ /************************************************************
63
+ * Open the viewer on `gclass_name`.
64
+ *
65
+ * opts:
66
+ * description the document to draw. Omitted, it is
67
+ * built from the browser registry.
68
+ * title_prefix what the window says before the gclass
69
+ * name (the yuno it belongs to).
70
+ * on_close called when the reader dismisses it.
71
+ *
72
+ * Returns a handle for close_gclass_view(), or null (and a
73
+ * logged reason) when there is nothing to show.
74
+ ************************************************************/
75
+ function open_gclass_view(host, gclass_name, opts)
76
+ {
77
+ opts = opts || {};
78
+
79
+ if(!host || !gclass_name) {
80
+ log_error(`yui_gclass_view: open without a host or a gclass name`);
81
+ return null;
82
+ }
83
+
84
+ let shell = yui_shell_of(host);
85
+
86
+ if(!gclass_view_available()) {
87
+ log_error(`${gobj_short_name(host)}: C_YUI_JSON not registered by the app`);
88
+ yui_shell_show_error(shell, "gclass viewer unavailable", {t: t});
89
+ return null;
90
+ }
91
+
92
+ let description = opts.description || describe_local_gclass(gclass_name);
93
+ if(!description) {
94
+ /* Not an error: a gclass of a BACKEND yuno is not in this
95
+ * registry and never will be. It is the caller's job to bring
96
+ * the description; say which gclass could not be found. */
97
+ log_warning(`${gobj_short_name(host)}: gclass not registered here: ${gclass_name}`);
98
+ yui_shell_show_error(shell, "gclass not registered in this yuno", {t: t});
99
+ return null;
100
+ }
101
+
102
+ let jv = gobj_create_service(
103
+ `gclass-view-${clean_name(gobj_name(host))}`,
104
+ "C_YUI_JSON",
105
+ {
106
+ /* No `title`: the host titles it -- the window's title bar
107
+ * on desktop, the dialog's header on mobile. The viewer's
108
+ * own title would land INSIDE that host, doubling it. */
109
+ json_data: description,
110
+ subscriber: null,
111
+ },
112
+ host
113
+ );
114
+ if(!jv) {
115
+ log_error(`${gobj_short_name(host)}: cannot create the gclass viewer`);
116
+ return null;
117
+ }
118
+
119
+ /* CREATED, not started. `mt_create` builds the DOM; `mt_start`
120
+ * RENDERS it -- and the viewer's graph view measures a canvas and
121
+ * puts a ResizeObserver on it, both of which need the element to be
122
+ * in the document. The window below is what puts it there, so the
123
+ * start goes after the window, at the bottom of this function.
124
+ *
125
+ * Starting it here worked for as long as the reader had last used
126
+ * the tree view: the graph is built on first entry, which was after
127
+ * the window. The moment the viewer REMEMBERED the graph view, it
128
+ * built it inside mt_start, detached, found no mount, attached no
129
+ * observer -- and the canvas kept its birth size for the life of
130
+ * the window while the window resized around it. */
131
+ let handle = {json_gobj: jv, win: null, modal: null};
132
+ let $box = gobj_read_pointer_attr(jv, "$container");
133
+
134
+ let on_close = () => {
135
+ if(typeof opts.on_close === "function") {
136
+ opts.on_close();
137
+ }
138
+ };
139
+
140
+ if(is_mobile()) {
141
+ if(!shell) {
142
+ log_error(`${gobj_short_name(host)}: no shell, cannot open the gclass sheet`);
143
+ close_gclass_view(handle);
144
+ return null;
145
+ }
146
+ handle.modal = yui_shell_show_modal(shell, $box, {
147
+ dialog: true,
148
+ logical_class: "GCLASS_VIEW_SHEET",
149
+ title_prefix: opts.title_prefix || "",
150
+ title: gclass_name,
151
+ t: t,
152
+ on_close: on_close,
153
+ });
154
+ gobj_start(jv); /* mounted: now it can measure itself */
155
+ return handle;
156
+ }
157
+
158
+ let $win_parent = window_layer_of(host, shell);
159
+
160
+ handle.win = gobj_create_service(
161
+ `gclass-win-${clean_name(gobj_name(host))}`,
162
+ "C_YUI_WINDOW",
163
+ {
164
+ $parent: $win_parent,
165
+ subscriber: null,
166
+ modal: false,
167
+ showMax: true,
168
+ showFooter: false,
169
+ resizable: true,
170
+ center: true,
171
+ auto_save_size_and_position: true,
172
+ width: 720,
173
+ height: 640,
174
+ logical_class: "GCLASS_VIEW_WINDOW",
175
+ title_prefix: opts.title_prefix || "",
176
+ title: gclass_name,
177
+ icon: "yi-code",
178
+ body: $box,
179
+ /* The same dock the other windows are on: raising a window
180
+ * is the manager's job, and a viewer registered nowhere
181
+ * cannot come to the front when the reader clicks it. `||
182
+ * null` because gobj_find_service answers undefined when
183
+ * absent, and an undefined attr logs "attr undefined". */
184
+ manager: gobj_find_service("__window_manager__", false) || null,
185
+ on_close: on_close,
186
+ },
187
+ host
188
+ );
189
+ if(!handle.win) {
190
+ log_error(`${gobj_short_name(host)}: cannot create the gclass window`);
191
+ close_gclass_view(handle);
192
+ return null;
193
+ }
194
+ gobj_start(handle.win);
195
+ gobj_start(jv); /* mounted: now it can measure itself */
196
+
197
+ return handle;
198
+ }
199
+
200
+ /************************************************************
201
+ * Close whatever open_gclass_view() opened. Safe on null and
202
+ * on a handle whose window the reader already dismissed.
203
+ ************************************************************/
204
+ function close_gclass_view(handle)
205
+ {
206
+ if(!handle) {
207
+ return;
208
+ }
209
+
210
+ let jv = handle.json_gobj;
211
+ let win = handle.win;
212
+ let modal = handle.modal;
213
+
214
+ handle.json_gobj = null;
215
+ handle.win = null;
216
+ handle.modal = null;
217
+
218
+ if(win && is_gobj(win)) {
219
+ try {
220
+ /* STOP, then destroy: gobj_destroy() raises the `destroying`
221
+ * flag before it can stop a running gobj, so destroying it
222
+ * straight logs two errors and skips mt_stop. The ✕ path
223
+ * already stopped it (close_window) -- hence the guard. */
224
+ if(gobj_is_running(win)) {
225
+ gobj_stop(win);
226
+ }
227
+ gobj_destroy(win);
228
+ } catch(e) {
229
+ log_warning(`yui_gclass_view: window already gone: ${e}`);
230
+ }
231
+ }
232
+ if(modal && typeof modal.close === "function") {
233
+ try {
234
+ modal.close();
235
+ } catch(e) {
236
+ log_warning(`yui_gclass_view: modal already gone: ${e}`);
237
+ }
238
+ }
239
+ if(jv && is_gobj(jv)) {
240
+ try {
241
+ if(gobj_is_running(jv)) {
242
+ gobj_stop(jv);
243
+ }
244
+ gobj_destroy(jv);
245
+ } catch(e) {
246
+ log_warning(`yui_gclass_view: viewer already gone: ${e}`);
247
+ }
248
+ }
249
+ }
250
+
251
+ /************************************************************
252
+ * Where to mount the window: the SAME element the caller's
253
+ * own window hangs from.
254
+ *
255
+ * Not a preference -- a requirement. z-index only orders
256
+ * siblings of one stacking context, so a viewer opened from a
257
+ * window on `body` and mounted in the shell's popup layer
258
+ * lands BEHIND its opener whatever number it is given, and
259
+ * the button reads as one that did nothing.
260
+ ************************************************************/
261
+ function window_layer_of(host, shell)
262
+ {
263
+ let $c = gobj_read_pointer_attr(host, "$container");
264
+ if($c && typeof $c.closest === "function") {
265
+ let $win = $c.closest(".C_YUI_WINDOW");
266
+ if($win && $win.parentNode) {
267
+ return $win.parentNode;
268
+ }
269
+ }
270
+
271
+ return (typeof document !== "undefined" &&
272
+ document.getElementById("top-layer")) ||
273
+ (shell && yui_shell_popup_layer(shell)) ||
274
+ null;
275
+ }
276
+
277
+ /************************************************************
278
+ * A window is not a sheet: below this width there is no room
279
+ * for a title bar the reader can drag.
280
+ ************************************************************/
281
+ function is_mobile()
282
+ {
283
+ return typeof window !== "undefined" && window.innerWidth <= 768;
284
+ }
285
+
286
+ export {
287
+ gclass_view_available,
288
+ open_gclass_view,
289
+ close_gclass_view,
290
+ };