@yuneta/gobj-ui 7.23.35 → 7.23.36

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,1522 @@
1
+ /***********************************************************************
2
+ * c_yui_gclass.js
3
+ *
4
+ * What a GClass IS, in zones.
5
+ *
6
+ * The framework answers `view-gclass` with a full description --
7
+ * attributes, commands, methods, trace levels and the FSM -- and until
8
+ * now the only thing that drew it was a JSON tree. Correct, and
9
+ * unreadable: the one question a reader arrives with ("what does this
10
+ * gclass DO in the state it is in?") took four expansions to reach.
11
+ *
12
+ * This viewer lays the same document out by zones, and reads BOTH
13
+ * dialects of it (the C kernel's and the browser registry's) through
14
+ * `gclass_view_model.js`, which is where every difference between them
15
+ * is resolved.
16
+ *
17
+ * THE MACHINE IS A MATRIX, rows = events and columns = states -- the
18
+ * shape the FSM is declared in, and the one that survives a gclass
19
+ * with 12 events and 3 states as well as one with 86 commands and one
20
+ * state. Two things it says that a JSON dump cannot:
21
+ *
22
+ * - an EMPTY CELL is information. An event with no action in the
23
+ * current state is not ignored in Yuneta, it is refused with
24
+ * "Event NOT DEFINED in state", so the empty cells are the map
25
+ * of what breaks. They are hatched, never left blank.
26
+ * - a state nothing declares a way INTO is marked. An action may
27
+ * jump with `gobj_change_state()` (C_IEVENT_CLI reaches
28
+ * ST_SESSION that way), and no description can see inside an
29
+ * action -- so the viewer flags the state instead of drawing
30
+ * the working half of a gclass as unreachable.
31
+ *
32
+ * TWO VIEWS over the same document, on the `view_mode` attr:
33
+ *
34
+ * zones the layout above.
35
+ * raw the description verbatim. It is the authoritative
36
+ * answer of the backend and is never thrown away.
37
+ *
38
+ * Container-agnostic, like C_YUI_JSON: the gclass owns the chrome and
39
+ * the host mounts `$container` wherever it wants (a C_YUI_WINDOW body
40
+ * on desktop, a dialog on a phone). `yui_gclass_view.js` does both.
41
+ *
42
+ * DOM is self-describing (UPPER_SNAKE logical classes): GCLASS_VIEWER /
43
+ * GCLASS_TOOLBAR / GCLASS_SEARCH / GCLASS_VIEW_SWITCH / GCLASS_ZONES /
44
+ * GCLASS_HEAD / GCLASS_ZONE / GCLASS_ZONE_TITLE / GCLASS_ZONE_BODY /
45
+ * GCLASS_TABLE / GCLASS_MATRIX / GCLASS_CELL / GCLASS_PUBLISHED /
46
+ * GCLASS_RAW.
47
+ *
48
+ * Copyright (c) 2026, ArtGins.
49
+ * All Rights Reserved.
50
+ ***********************************************************************/
51
+ import {
52
+ SDATA,
53
+ SDATA_END,
54
+ data_type_t,
55
+ gclass_create,
56
+ gclass_find_by_name,
57
+ log_error,
58
+ gobj_read_pointer_attr,
59
+ gobj_parent,
60
+ gobj_subscribe_event,
61
+ gobj_read_attr,
62
+ gobj_write_attr,
63
+ gobj_read_str_attr,
64
+ gobj_send_event,
65
+ gobj_create_pure_child,
66
+ gobj_start,
67
+ gobj_stop,
68
+ gobj_destroy,
69
+ gobj_is_running,
70
+ gobj_short_name,
71
+ gobj_name,
72
+ clean_name,
73
+ createElement2,
74
+ json_deep_copy,
75
+ refresh_language,
76
+ } from "@yuneta/gobj-js";
77
+
78
+ import {gclass_view_model} from "./gclass_view_model.js";
79
+ import {register_c_yui_fsm_graph} from "./c_yui_fsm_graph.js";
80
+ import {json_text_dump} from "./json_view_helpers.js";
81
+ import {yui_toolbar} from "./yui_toolbar.js";
82
+ import {attach_clear} from "./yui_inputs.js";
83
+ import {yui_copy_json, yui_button_mark_done} from "./yui_clipboard.js";
84
+
85
+ import {t} from "i18next";
86
+
87
+ import "./c_yui_gclass.css";
88
+
89
+
90
+ /***************************************************************
91
+ * Constants
92
+ ***************************************************************/
93
+ const GCLASS_NAME = "C_YUI_GCLASS";
94
+
95
+ /*
96
+ * The two views, in switch order.
97
+ */
98
+ const VIEWS = [
99
+ {mode: "zones", icon: "yi-table", key: "zones view"},
100
+ {mode: "raw", icon: "yi-code", key: "raw view"},
101
+ ];
102
+
103
+ /*
104
+ * The two ways to read the machine, in switch order.
105
+ *
106
+ * The matrix leads and stays the default: it is the shape the FSM is
107
+ * DECLARED in, it reads every gclass the same way, and it is the only
108
+ * one of the two that can show what is MISSING. The graph answers a
109
+ * different question -- where does this machine go -- and earns its
110
+ * place exactly where the matrix is worst: many states, few events
111
+ * each.
112
+ */
113
+ const MACHINE_VIEWS = [
114
+ {mode: "matrix", icon: "yi-table", key: "matrix view"},
115
+ {mode: "graph", icon: "yi-hexagon-nodes", key: "graph view"},
116
+ ];
117
+
118
+ /*
119
+ * Hard cap on the characters the raw view prints. The same guard the
120
+ * JSON viewer applies one layer down, and announced rather than
121
+ * applied silently.
122
+ */
123
+ const MAX_RAW_CHARS = 2000000;
124
+
125
+ /*
126
+ * The zones, in the order they are laid out. `col` is which of the two
127
+ * columns a zone belongs to on a wide container -- the machine has one
128
+ * to itself because it is the only zone that grows sideways.
129
+ */
130
+ const ZONES = [
131
+ {id: "attrs", key: "attributes", col: "left"},
132
+ {id: "commands", key: "commands", col: "left"},
133
+ {id: "methods", key: "methods", col: "left"},
134
+ {id: "traces", key: "trace levels", col: "left"},
135
+ {id: "machine", key: "machine", col: "right"},
136
+ ];
137
+
138
+ /***************************************************************
139
+ * Data
140
+ ***************************************************************/
141
+ const attrs_table = [
142
+ /*---------------- Public Attributes ----------------*/
143
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
144
+
145
+ /*---------------- Config ----------------*/
146
+ SDATA(data_type_t.DTP_JSON, "description", 0, null, "The gclass description, as `view-gclass` answers it or `describe_local_gclass()` builds it"),
147
+ SDATA(data_type_t.DTP_STRING, "gclass_name", 0, "", "Name to head the view with when the description carries none"),
148
+ SDATA(data_type_t.DTP_STRING, "current_state",0, "", "State of the INSTANCE the host is showing, lit in the machine. Empty when the host is not showing one"),
149
+ SDATA(data_type_t.DTP_STRING, "view_mode", 0, "zones","View: 'zones' or 'raw'"),
150
+
151
+ /*---------------- UI ----------------*/
152
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
153
+ SDATA_END()
154
+ ];
155
+
156
+ let PRIVATE_DATA = {
157
+ model: null, // the view model built from the description
158
+ search: "", // current filter term (lower-cased)
159
+ collapsed: null, // Set<string> of collapsed zone ids
160
+ machine_view: "matrix", // how the machine zone is read
161
+ graph_gobj: null, // hosted C_YUI_FSM_GRAPH child (built on first use)
162
+ $zones: null, // the scrollable zones body
163
+ $raw: null, // the scrollable raw body
164
+ $raw_body: null, // the <pre> inside it
165
+ $search: null, // the search input
166
+ $mode_btns: null, // Map<mode, HTMLElement> of the view switch
167
+ };
168
+
169
+ let __gclass__ = null;
170
+
171
+
172
+
173
+
174
+ /******************************
175
+ * Framework Methods
176
+ ******************************/
177
+
178
+
179
+
180
+
181
+ /***************************************************************
182
+ * Framework Method: Create
183
+ ***************************************************************/
184
+ function mt_create(gobj)
185
+ {
186
+ let priv = gobj.priv;
187
+
188
+ priv.collapsed = new Set();
189
+ priv.search = "";
190
+ priv.machine_view = "matrix";
191
+ priv.model = gclass_view_model(gobj_read_attr(gobj, "description"));
192
+
193
+ build_ui(gobj);
194
+ apply_view_mode(gobj);
195
+
196
+ /*
197
+ * CHILD subscription model
198
+ */
199
+ let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
200
+ if(!subscriber) {
201
+ subscriber = gobj_parent(gobj);
202
+ }
203
+ gobj_subscribe_event(gobj, null, {}, subscriber);
204
+ }
205
+
206
+ /***************************************************************
207
+ * Framework Method: Start
208
+ ***************************************************************/
209
+ function mt_start(gobj)
210
+ {
211
+ render_view(gobj);
212
+ }
213
+
214
+ /***************************************************************
215
+ * Framework Method: Stop
216
+ ***************************************************************/
217
+ function mt_stop(gobj)
218
+ {
219
+ /*
220
+ * The graph child dies with this gobj's RUNNING state, not with
221
+ * its destruction: gobj_destroy() destroys the children BEFORE
222
+ * calling mt_destroy(), so tearing it down there arrives after
223
+ * the framework already destroyed it -- while it was running.
224
+ */
225
+ teardown_graph_child(gobj);
226
+ }
227
+
228
+ /***************************************************************
229
+ * Framework Method: Destroy
230
+ ***************************************************************/
231
+ function mt_destroy(gobj)
232
+ {
233
+ destroy_ui(gobj);
234
+ }
235
+
236
+
237
+
238
+
239
+ /***************************
240
+ * Local Methods
241
+ ***************************/
242
+
243
+
244
+
245
+
246
+ /************************************************************
247
+ * Build UI
248
+ ************************************************************/
249
+ function build_ui(gobj)
250
+ {
251
+ let priv = gobj.priv;
252
+
253
+ let $toolbar = make_toolbar(gobj);
254
+
255
+ let $container = createElement2(
256
+ ['div', {class: 'C_YUI_GCLASS GCLASS_VIEWER view-card',
257
+ style: 'height:100%; display:flex; flex-direction:column;'}, [
258
+ ['div', {class: 'GCLASS_TOOLBAR is-flex-grow-0'}, [$toolbar]],
259
+ ['div', {class: 'GCLASS_ZONES is-flex-grow-1',
260
+ style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []],
261
+ ['div', {class: 'GCLASS_RAW is-flex-grow-1 is-hidden',
262
+ style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []]
263
+ ]]
264
+ );
265
+
266
+ gobj_write_attr(gobj, "$container", $container);
267
+ priv.$zones = $container.querySelector('.GCLASS_ZONES');
268
+ priv.$raw = $container.querySelector('.GCLASS_RAW');
269
+ priv.$search = $container.querySelector('.GCLASS_SEARCH');
270
+ priv.$mode_btns = new Map();
271
+ VIEWS.forEach(function(v) {
272
+ let $btn = $container.querySelector('.GCLASS_VIEW_MODE_' + v.mode.toUpperCase());
273
+ if($btn) {
274
+ priv.$mode_btns.set(v.mode, $btn);
275
+ }
276
+ });
277
+
278
+ refresh_language($container, t);
279
+ }
280
+
281
+ /************************************************************
282
+ * Destroy UI
283
+ ************************************************************/
284
+ function destroy_ui(gobj)
285
+ {
286
+ let priv = gobj.priv;
287
+ let $container = gobj_read_attr(gobj, "$container");
288
+ if($container) {
289
+ if($container.parentNode) {
290
+ $container.parentNode.removeChild($container);
291
+ }
292
+ gobj_write_attr(gobj, "$container", null);
293
+ }
294
+ priv.$zones = null;
295
+ priv.$raw = null;
296
+ priv.$raw_body = null;
297
+ priv.$search = null;
298
+ priv.$mode_btns = null;
299
+ }
300
+
301
+ /************************************************************
302
+ * Toolbar: a filter, the view switch and copy.
303
+ ************************************************************/
304
+ function make_toolbar(gobj)
305
+ {
306
+ let $search_input = createElement2(
307
+ ['input', {class: 'GCLASS_SEARCH input', type: 'text',
308
+ placeholder: t("search")}, [], {
309
+ input: function(evt) {
310
+ gobj_send_event(gobj, "EV_SEARCH", {text: evt.target.value}, gobj);
311
+ }
312
+ }]);
313
+ let $search_control = createElement2(
314
+ ['div', {class: 'control has-icons-left GCLASS_SEARCH_CONTROL',
315
+ style: 'max-width:22em;'}, [
316
+ $search_input,
317
+ ['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
318
+ ]]);
319
+ attach_clear($search_control, $search_input);
320
+
321
+ let right_items = [
322
+ view_mode_switch(gobj),
323
+ icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
324
+ ];
325
+
326
+ const $toolbar = yui_toolbar({}, [
327
+ ['div', {class: 'yui-horizontal-toolbar-section left is-flex is-align-items-center',
328
+ style: 'gap:.25rem;'}, [$search_control]],
329
+ ['div', {class: 'yui-horizontal-toolbar-section center'}, []],
330
+ ['div', {class: 'yui-horizontal-toolbar-section right is-flex is-align-items-center',
331
+ style: 'gap:.25rem;'}, right_items],
332
+ ]);
333
+
334
+ refresh_language($toolbar, t);
335
+ return $toolbar;
336
+ }
337
+
338
+ /************************************************************
339
+ * A single icon toolbar button that fires `event_name`
340
+ ************************************************************/
341
+ function icon_button(gobj, icon, event_name, label_key)
342
+ {
343
+ return ['button', {class: `button ${event_name}`, style: 'width:2.5em;',
344
+ title: t(label_key), 'data-i18n-title': label_key,
345
+ 'aria-label': t(label_key), 'data-i18n-aria-label': label_key}, [
346
+ ['span', {class: 'icon'}, [['i', {class: icon}]]]
347
+ ], {
348
+ click: function(evt) {
349
+ evt.stopPropagation();
350
+ gobj_send_event(gobj, event_name, {}, gobj);
351
+ }
352
+ }];
353
+ }
354
+
355
+ /************************************************************
356
+ * The view switch: one button per view, the current one
357
+ * marked. Two buttons and not a toggle, for the same reason
358
+ * the JSON viewer gives three: a control has to say where it
359
+ * goes, not that it will change something.
360
+ ************************************************************/
361
+ function view_mode_switch(gobj)
362
+ {
363
+ let buttons = VIEWS.map(function(v) {
364
+ return ['button', {class: `button GCLASS_VIEW_MODE GCLASS_VIEW_MODE_${v.mode.toUpperCase()}`,
365
+ type: 'button', style: 'width:2.5em;',
366
+ title: t(v.key), 'data-i18n-title': v.key,
367
+ 'aria-label': t(v.key), 'data-i18n-aria-label': v.key}, [
368
+ ['span', {class: 'icon'}, [['i', {class: v.icon}]]]
369
+ ], {
370
+ click: function(evt) {
371
+ evt.stopPropagation();
372
+ gobj_send_event(gobj, "EV_SET_VIEW_MODE", {mode: v.mode}, gobj);
373
+ }
374
+ }];
375
+ });
376
+
377
+ return ['div', {class: 'buttons has-addons is-flex-wrap-nowrap mb-0 GCLASS_VIEW_SWITCH'},
378
+ buttons];
379
+ }
380
+
381
+ /************************************************************
382
+ * The current view, normalised. Anything unknown is zones.
383
+ ************************************************************/
384
+ function current_view_mode(gobj)
385
+ {
386
+ let mode = gobj_read_str_attr(gobj, "view_mode");
387
+ return VIEWS.some((v) => v.mode === mode)? mode: "zones";
388
+ }
389
+
390
+ /************************************************************
391
+ * Show the chrome of the current view and hide the other's.
392
+ ************************************************************/
393
+ function apply_view_mode(gobj)
394
+ {
395
+ let priv = gobj.priv;
396
+ let mode = current_view_mode(gobj);
397
+
398
+ let bodies = {zones: priv.$zones, raw: priv.$raw};
399
+ for(let [m, $el] of Object.entries(bodies)) {
400
+ if($el) {
401
+ $el.classList.toggle('is-hidden', m !== mode);
402
+ }
403
+ }
404
+
405
+ if(priv.$mode_btns) {
406
+ for(let [m, $btn] of priv.$mode_btns) {
407
+ $btn.classList.toggle('is-active', m === mode);
408
+ $btn.setAttribute("aria-pressed", (m === mode)? "true": "false");
409
+ }
410
+ }
411
+ }
412
+
413
+ /************************************************************
414
+ * Render whichever view is current.
415
+ ************************************************************/
416
+ function render_view(gobj)
417
+ {
418
+ if(current_view_mode(gobj) === "raw") {
419
+ render_raw(gobj);
420
+ } else {
421
+ render_zones(gobj);
422
+ }
423
+ }
424
+
425
+ /************************************************************
426
+ * The raw view: the description verbatim, indented four.
427
+ ************************************************************/
428
+ function render_raw(gobj)
429
+ {
430
+ let priv = gobj.priv;
431
+ let $raw = priv.$raw;
432
+ if(!$raw) {
433
+ return;
434
+ }
435
+
436
+ $raw.textContent = "";
437
+ priv.$raw_body = null;
438
+
439
+ let description = gobj_read_attr(gobj, "description");
440
+ if(description === null || description === undefined) {
441
+ $raw.appendChild(createElement2(
442
+ ['div', {class: 'GCLASS_EMPTY p-4 has-text-grey',
443
+ i18n: 'no data'}, t("no data")]));
444
+ return;
445
+ }
446
+
447
+ let dump = json_text_dump(description, MAX_RAW_CHARS);
448
+ let children = [
449
+ ['pre', {class: 'GCLASS_RAW_BODY'}, dump.text]
450
+ ];
451
+ if(dump.capped) {
452
+ children.push(
453
+ ['div', {class: 'GCLASS_RAW_CAPPED has-text-grey is-size-7 p-2',
454
+ i18n: 'text truncated'}, t("text truncated")]);
455
+ }
456
+ if(dump.error) {
457
+ children.push(
458
+ ['div', {class: 'GCLASS_RAW_ERROR has-text-danger is-size-7 p-2'},
459
+ dump.error]);
460
+ }
461
+
462
+ let $body = createElement2(['div', {class: 'GCLASS_RAW_WRAP'}, children]);
463
+ $raw.appendChild($body);
464
+ priv.$raw_body = $body.querySelector('.GCLASS_RAW_BODY');
465
+ refresh_language($raw, t);
466
+ }
467
+
468
+ /************************************************************
469
+ * The zones view.
470
+ ************************************************************/
471
+ function render_zones(gobj)
472
+ {
473
+ let priv = gobj.priv;
474
+ let $zones = priv.$zones;
475
+ if(!$zones) {
476
+ return;
477
+ }
478
+
479
+ /* The subtree it lives in is about to be thrown away. */
480
+ teardown_graph_child(gobj);
481
+
482
+ $zones.textContent = "";
483
+
484
+ let model = priv.model;
485
+ if(!model) {
486
+ $zones.appendChild(createElement2(
487
+ ['div', {class: 'GCLASS_EMPTY p-4 has-text-grey',
488
+ i18n: 'no data'}, t("no data")]));
489
+ return;
490
+ }
491
+
492
+ /*
493
+ * The machine takes the WHOLE width in graph mode.
494
+ *
495
+ * Half a column is enough for a matrix, which grows downwards and
496
+ * scrolls sideways inside its own box. A graph does not: it is
497
+ * laid out left to right, and 600px of a 1200px viewer is where
498
+ * six states stop fitting and start being panned. The rest of the
499
+ * zones then stack in one column under it, which is what they do
500
+ * on a phone anyway.
501
+ */
502
+ let graph_mode = (priv.machine_view === "graph");
503
+ let left = [];
504
+ let right = [];
505
+ let full = [];
506
+
507
+ for(let zone of ZONES) {
508
+ let $zone = build_zone(gobj, zone);
509
+ if(!$zone) {
510
+ continue;
511
+ }
512
+ if(zone.col === "right") {
513
+ if(graph_mode) {
514
+ full.push($zone);
515
+ } else {
516
+ right.push($zone);
517
+ }
518
+ } else {
519
+ left.push($zone);
520
+ }
521
+ }
522
+
523
+ let $root = createElement2(
524
+ ['div', {class: 'GCLASS_BODY'}, [
525
+ build_head(gobj, model),
526
+ ['div', {class: 'GCLASS_FULL' + (graph_mode? '': ' is-hidden')}, full],
527
+ ['div', {class: 'GCLASS_COLS' + (graph_mode? ' is-stacked': '')}, [
528
+ ['div', {class: 'GCLASS_COL GCLASS_COL_LEFT'}, left],
529
+ ['div', {class: 'GCLASS_COL GCLASS_COL_RIGHT' +
530
+ (graph_mode? ' is-hidden': '')}, right]
531
+ ]]
532
+ ]]
533
+ );
534
+
535
+ $zones.appendChild($root);
536
+ refresh_language($zones, t);
537
+ apply_search(gobj);
538
+ reveal_current_state(gobj);
539
+
540
+ if(priv.machine_view === "graph" && !priv.collapsed.has("machine")) {
541
+ build_graph_child(gobj);
542
+ }
543
+ }
544
+
545
+ /************************************************************
546
+ * The header: what this gclass is called, and the two
547
+ * numbers a reader asks for before anything else.
548
+ ************************************************************/
549
+ function build_head(gobj, model)
550
+ {
551
+ let items = [
552
+ ['span', {class: 'GCLASS_NAME'},
553
+ model.id || gobj_read_str_attr(gobj, "gclass_name") || "?"]
554
+ ];
555
+
556
+ if(model.instances !== null) {
557
+ items.push(
558
+ ['span', {class: 'GCLASS_INSTANCES tag'}, [
559
+ ['span', {class: 'GCLASS_INSTANCES_N'}, String(model.instances)],
560
+ ['span', {class: 'GCLASS_INSTANCES_LABEL', i18n: 'instances'},
561
+ t("instances")]
562
+ ]]);
563
+ }
564
+
565
+ for(let flag of model.gcflag) {
566
+ items.push(['span', {class: 'GCLASS_CHIP GCLASS_GCFLAG'}, flag]);
567
+ }
568
+
569
+ let current = gobj_read_str_attr(gobj, "current_state");
570
+ if(current) {
571
+ items.push(
572
+ ['span', {class: 'GCLASS_CHIP GCLASS_CURRENT',
573
+ title: t("current state"),
574
+ 'data-i18n-title': 'current state'}, current]);
575
+ }
576
+
577
+ return ['div', {class: 'GCLASS_HEAD'}, items];
578
+ }
579
+
580
+ /************************************************************
581
+ * One zone: a heading that folds it, and its body.
582
+ *
583
+ * Every zone is drawn, empty ones included: "this gclass
584
+ * declares no commands" is an answer, and a zone that
585
+ * disappears makes the reader wonder whether the viewer
586
+ * simply failed to draw it.
587
+ ************************************************************/
588
+ function build_zone(gobj, zone)
589
+ {
590
+ let priv = gobj.priv;
591
+ let model = priv.model;
592
+
593
+ let body = null;
594
+ let count = 0;
595
+
596
+ switch(zone.id) {
597
+ case "attrs":
598
+ count = model.attrs.length;
599
+ body = build_attrs(model.attrs);
600
+ break;
601
+ case "commands":
602
+ count = model.commands.filter((c) => c.kind === "command").length;
603
+ body = build_commands(model.commands);
604
+ break;
605
+ case "methods":
606
+ count = model.methods.length + model.internal_methods.length;
607
+ body = build_methods(model);
608
+ break;
609
+ case "traces":
610
+ count = model.trace_levels.length;
611
+ body = build_traces(model.trace_levels);
612
+ break;
613
+ case "machine":
614
+ /* Two numbers, because one of them alone says nothing
615
+ * about the shape: 13 events across 1 state and 13
616
+ * across 6 are different machines. */
617
+ count = model.fsm.states.length?
618
+ `${model.fsm.states.length} · ${model.fsm.rows.length}`:
619
+ model.fsm.rows.length;
620
+ body = build_machine(gobj, model.fsm);
621
+ break;
622
+ default:
623
+ log_error(`${GCLASS_NAME}: unknown zone '${zone.id}'`);
624
+ return null;
625
+ }
626
+
627
+ let collapsed = priv.collapsed.has(zone.id);
628
+
629
+ let $toggle = createElement2(
630
+ ['button', {class: 'GCLASS_ZONE_TOGGLE', type: 'button',
631
+ 'aria-expanded': collapsed? 'false': 'true'}, [
632
+ ['span', {class: 'icon GCLASS_ZONE_ARROW'}, [
633
+ ['i', {class: collapsed? 'yi-chevron-right': 'yi-chevron-down'}]
634
+ ]],
635
+ ['span', {class: 'GCLASS_ZONE_TITLE', i18n: zone.key}, t(zone.key)],
636
+ ['span', {class: 'GCLASS_ZONE_COUNT'}, String(count)]
637
+ ], {
638
+ click: function(evt) {
639
+ evt.stopPropagation();
640
+ gobj_send_event(gobj, "EV_TOGGLE_ZONE", {zone: zone.id}, gobj);
641
+ }
642
+ }]);
643
+
644
+ return createElement2(
645
+ ['section', {class: `GCLASS_ZONE GCLASS_ZONE_${zone.id.toUpperCase()}` +
646
+ (collapsed? ' is-collapsed': ''),
647
+ 'data-zone': zone.id}, [
648
+ $toggle,
649
+ ['div', {class: 'GCLASS_ZONE_BODY' + (collapsed? ' is-hidden': '')},
650
+ [body]]
651
+ ]]);
652
+ }
653
+
654
+ /************************************************************
655
+ * An "empty zone" line. It says WHY it is empty, which is
656
+ * never the same as saying nothing.
657
+ ************************************************************/
658
+ function empty_line(key)
659
+ {
660
+ return ['div', {class: 'GCLASS_ZONE_EMPTY', i18n: key}, t(key)];
661
+ }
662
+
663
+ /************************************************************
664
+ * A table in its own horizontal scroller.
665
+ *
666
+ * EVERY table, not just the matrix. An attrs table with a
667
+ * long description is as wide as a matrix with eight states,
668
+ * and on a phone the one that overflows takes the whole
669
+ * viewer sideways with it -- the page ends up scrolling, and
670
+ * the toolbar leaves the screen.
671
+ ************************************************************/
672
+ function scroller(table)
673
+ {
674
+ return ['div', {class: 'GCLASS_SCROLL'}, [table]];
675
+ }
676
+
677
+ /************************************************************
678
+ * A row's haystack: what the filter matches against. Built
679
+ * once, at render, and kept on the element -- filtering a
680
+ * table by reading its cells back out is how a filter starts
681
+ * matching the words of the CHROME.
682
+ ************************************************************/
683
+ function row_attrs(haystack, extra)
684
+ {
685
+ let attrs = {class: 'GCLASS_ROW', 'data-find': String(haystack).toLowerCase()};
686
+ return Object.assign(attrs, extra || {});
687
+ }
688
+
689
+ /************************************************************
690
+ * Flags, as chips. A flag that changes what a reader may DO
691
+ * with the attr gets its own class; the rest are quiet.
692
+ ************************************************************/
693
+ function flag_chips(flags)
694
+ {
695
+ return flags.map(function(flag) {
696
+ let cls = 'GCLASS_CHIP GCLASS_FLAG';
697
+ if(flag.indexOf("PERSIST") >= 0) {
698
+ cls += ' is-persist';
699
+ } else if(flag.indexOf("WR") >= 0) {
700
+ cls += ' is-writable';
701
+ } else if(flag.indexOf("AUTHZ") >= 0) {
702
+ cls += ' is-authz';
703
+ } else if(flag.indexOf("REQUIRED") >= 0) {
704
+ cls += ' is-required';
705
+ }
706
+ return ['span', {class: cls}, flag];
707
+ });
708
+ }
709
+
710
+ /************************************************************
711
+ * The attrs zone.
712
+ ************************************************************/
713
+ function build_attrs(attrs)
714
+ {
715
+ if(!attrs.length) {
716
+ return empty_line("no attributes declared");
717
+ }
718
+
719
+ let rows = attrs.map(function(a) {
720
+ return ['tr', row_attrs(`${a.id} ${a.type} ${a.flags.join(" ")} ${a.description}`), [
721
+ ['td', {class: 'GCLASS_CELL_KEY'}, a.id],
722
+ ['td', {class: 'GCLASS_CELL_TYPE'}, a.type],
723
+ ['td', {class: 'GCLASS_CELL_FLAGS'}, flag_chips(a.flags)],
724
+ ['td', {class: 'GCLASS_CELL_DEFAULT'}, a.default_value],
725
+ ['td', {class: 'GCLASS_CELL_DESC'}, a.description]
726
+ ]];
727
+ });
728
+
729
+ return scroller(['table', {class: 'GCLASS_TABLE GCLASS_TABLE_ATTRS'}, [
730
+ ['thead', {}, [
731
+ ['tr', {}, [
732
+ ['th', {i18n: 'name'}, t("name")],
733
+ ['th', {i18n: 'type'}, t("type")],
734
+ ['th', {i18n: 'flags'}, t("flags")],
735
+ ['th', {i18n: 'default'}, t("default")],
736
+ ['th', {i18n: 'description'}, t("description")]
737
+ ]]
738
+ ]],
739
+ ['tbody', {}, rows]
740
+ ]]);
741
+ }
742
+
743
+ /************************************************************
744
+ * The commands zone.
745
+ *
746
+ * A command's parameters ride under it as their own rows,
747
+ * indented: they are what the reader needs the moment they
748
+ * decide to run it, and a second click to reach them buys
749
+ * nothing on a list this short.
750
+ ************************************************************/
751
+ function build_commands(commands)
752
+ {
753
+ if(!commands.length) {
754
+ return empty_line("no commands declared");
755
+ }
756
+
757
+ let rows = [];
758
+ for(let c of commands) {
759
+ if(c.kind === "section") {
760
+ rows.push(['tr', row_attrs(c.label, {class: 'GCLASS_ROW GCLASS_CMD_SECTION'}), [
761
+ ['td', {colspan: '2'}, c.label]
762
+ ]]);
763
+ continue;
764
+ }
765
+
766
+ let name_items = [['span', {class: 'GCLASS_CMD_NAME'}, c.id]];
767
+ for(let alias of c.alias) {
768
+ name_items.push(['span', {class: 'GCLASS_CHIP GCLASS_ALIAS'}, alias]);
769
+ }
770
+ name_items = name_items.concat(flag_chips(c.flags));
771
+
772
+ rows.push(['tr', row_attrs(`${c.id} ${c.alias.join(" ")} ${c.description}`), [
773
+ ['td', {class: 'GCLASS_CELL_KEY'}, name_items],
774
+ ['td', {class: 'GCLASS_CELL_DESC'}, c.description]
775
+ ]]);
776
+
777
+ for(let p of c.parameters) {
778
+ rows.push(['tr', row_attrs(`${c.id} ${p.id} ${p.description}`,
779
+ {class: 'GCLASS_ROW GCLASS_CMD_PARAM'}), [
780
+ ['td', {class: 'GCLASS_CELL_KEY'}, [
781
+ ['span', {class: 'GCLASS_PARAM_MARK'}, "↳"],
782
+ ['span', {class: 'GCLASS_PARAM_NAME'}, p.id],
783
+ ['span', {class: 'GCLASS_PARAM_TYPE'}, p.type]
784
+ ].concat(flag_chips(p.flags))],
785
+ ['td', {class: 'GCLASS_CELL_DESC'}, p.description]
786
+ ]]);
787
+ }
788
+ }
789
+
790
+ return scroller(['table', {class: 'GCLASS_TABLE GCLASS_TABLE_COMMANDS'}, [
791
+ ['tbody', {}, rows]
792
+ ]]);
793
+ }
794
+
795
+ /************************************************************
796
+ * The methods zone: the ones the gclass IMPLEMENTS.
797
+ *
798
+ * A table of every possible method, mostly empty, tells the
799
+ * reader nothing -- so both sides send only what is wired,
800
+ * and this draws them as chips.
801
+ ************************************************************/
802
+ function build_methods(model)
803
+ {
804
+ if(!model.methods.length && !model.internal_methods.length) {
805
+ return empty_line("no methods declared");
806
+ }
807
+
808
+ let blocks = [];
809
+ if(model.methods.length) {
810
+ blocks.push(['div', {class: 'GCLASS_CHIPS'},
811
+ model.methods.map((m) =>
812
+ ['span', row_attrs(m, {class: 'GCLASS_CHIP GCLASS_METHOD'}), m])]);
813
+ }
814
+ if(model.internal_methods.length) {
815
+ blocks.push(['div', {class: 'GCLASS_SUBTITLE', i18n: 'internal methods'},
816
+ t("internal methods")]);
817
+ blocks.push(['div', {class: 'GCLASS_CHIPS'},
818
+ model.internal_methods.map((m) =>
819
+ ['span', row_attrs(m, {class: 'GCLASS_CHIP GCLASS_METHOD'}), m])]);
820
+ }
821
+
822
+ return ['div', {class: 'GCLASS_METHODS'}, blocks];
823
+ }
824
+
825
+ /************************************************************
826
+ * The trace levels zone.
827
+ ************************************************************/
828
+ function build_traces(levels)
829
+ {
830
+ if(!levels.length) {
831
+ return empty_line("no trace levels declared");
832
+ }
833
+
834
+ let rows = levels.map(function(l) {
835
+ return ['tr', row_attrs(`${l.id} ${l.description}`), [
836
+ ['td', {class: 'GCLASS_CELL_KEY'}, l.id],
837
+ ['td', {class: 'GCLASS_CELL_DESC'}, l.description]
838
+ ]];
839
+ });
840
+
841
+ return scroller(['table', {class: 'GCLASS_TABLE GCLASS_TABLE_TRACES'}, [
842
+ ['tbody', {}, rows]
843
+ ]]);
844
+ }
845
+
846
+ /************************************************************
847
+ * The machine: the matrix, then what it publishes.
848
+ ************************************************************/
849
+ function build_machine(gobj, fsm)
850
+ {
851
+ let priv = gobj.priv;
852
+
853
+ if(!fsm.states.length) {
854
+ return empty_line("no machine declared");
855
+ }
856
+
857
+ if(priv.machine_view === "graph") {
858
+ /* An empty mount: the child is created AFTER this subtree is
859
+ * in the document, because G6 sizes itself from its container
860
+ * and one that is still detached measures 0x0. */
861
+ return ['div', {class: 'GCLASS_MACHINE'}, [
862
+ machine_view_switch(gobj),
863
+ ['div', {class: 'GCLASS_GRAPH_MOUNT'}, []]
864
+ ]];
865
+ }
866
+
867
+ let current = gobj_read_str_attr(gobj, "current_state");
868
+ let unreachable = new Set(fsm.unreachable);
869
+
870
+ let head_cells = [
871
+ ['th', {class: 'GCLASS_MX_CORNER', i18n: 'event'}, t("event")]
872
+ ];
873
+ for(let state of fsm.states) {
874
+ let cls = 'GCLASS_MX_STATE';
875
+ let items = [['span', {class: 'GCLASS_MX_STATE_NAME'}, state.name]];
876
+
877
+ if(state.name === current) {
878
+ cls += ' is-current';
879
+ }
880
+ if(unreachable.has(state.name)) {
881
+ cls += ' is-unreachable';
882
+ items.push(['span', {class: 'GCLASS_MX_MARK',
883
+ title: t("state with no declared entry"),
884
+ 'data-i18n-title': 'state with no declared entry',
885
+ 'aria-label': t("state with no declared entry"),
886
+ 'data-i18n-aria-label': 'state with no declared entry'},
887
+ "⚠"]);
888
+ }
889
+ head_cells.push(['th', {class: cls}, items]);
890
+ }
891
+
892
+ let body_rows = fsm.rows.map(function(row) {
893
+ let cells = [
894
+ ['td', {class: 'GCLASS_MX_EVENT'},
895
+ [['span', {}, row.event]].concat(event_flag_chips(row.flags))]
896
+ ];
897
+ row.cells.forEach(function(cell, i) {
898
+ let state = fsm.states[i];
899
+ let is_current = (state && state.name === current);
900
+
901
+ if(!cell) {
902
+ cells.push(['td', {class: 'GCLASS_MX_CELL is-none' +
903
+ (is_current? ' is-current': ''),
904
+ title: t("not declared in this state"),
905
+ 'data-i18n-title': 'not declared in this state'},
906
+ "·"]);
907
+ return;
908
+ }
909
+
910
+ let items = [];
911
+ if(cell.action) {
912
+ items.push(['span', {class: 'GCLASS_MX_ACTION'}, cell.action]);
913
+ } else {
914
+ /* The C side cannot name an action: `states2json()`
915
+ * writes the literal "action" for all of them. A mark
916
+ * is the honest drawing of "there is one". */
917
+ items.push(['span', {class: 'GCLASS_MX_ACTION is-unnamed',
918
+ title: t("action"),
919
+ 'data-i18n-title': 'action'}, "●"]);
920
+ }
921
+ if(cell.next_state) {
922
+ items.push(['span', {class: 'GCLASS_MX_ARROW'}, "▸"]);
923
+ items.push(['span', {class: 'GCLASS_MX_NEXT'}, cell.next_state]);
924
+ }
925
+
926
+ cells.push(['td', {class: 'GCLASS_MX_CELL' +
927
+ (is_current? ' is-current': '')}, items]);
928
+ });
929
+
930
+ return ['tr', row_attrs(`${row.event} ${row.cells.map(
931
+ (c) => c? `${c.action} ${c.next_state}`: "").join(" ")}`), cells];
932
+ });
933
+
934
+ let blocks = [
935
+ machine_view_switch(gobj),
936
+ scroller(['table', {class: 'GCLASS_TABLE GCLASS_MATRIX'}, [
937
+ ['thead', {}, [['tr', {}, head_cells]]],
938
+ ['tbody', {}, body_rows]
939
+ ]])
940
+ ];
941
+
942
+ blocks.push(build_legend(fsm, current));
943
+
944
+ if(fsm.published.length) {
945
+ blocks.push(['div', {class: 'GCLASS_SUBTITLE', i18n: 'publishes'},
946
+ t("publishes")]);
947
+ blocks.push(['div', {class: 'GCLASS_CHIPS GCLASS_PUBLISHED'},
948
+ fsm.published.map(function(p) {
949
+ return ['span', row_attrs(p.event, {class: 'GCLASS_PUB_ROW'}), [
950
+ ['span', {class: 'GCLASS_CHIP GCLASS_PUB_EVENT'}, p.event]
951
+ ].concat(event_flag_chips(p.flags))];
952
+ })]);
953
+ }
954
+
955
+ return ['div', {class: 'GCLASS_MACHINE'}, blocks];
956
+ }
957
+
958
+ /************************************************************
959
+ * Matrix or graph, for the machine zone alone.
960
+ *
961
+ * It lives INSIDE the zone and not in the toolbar: it
962
+ * changes one zone, and a control that changes one thing
963
+ * belongs beside that thing. The zone heading cannot hold it
964
+ * either -- the heading is itself a button, and a button
965
+ * inside a button is not a control, it is a bug.
966
+ ************************************************************/
967
+ function machine_view_switch(gobj)
968
+ {
969
+ let priv = gobj.priv;
970
+
971
+ let buttons = MACHINE_VIEWS.map(function(v) {
972
+ let on = (priv.machine_view === v.mode);
973
+ return ['button', {class: 'button is-small GCLASS_MACHINE_VIEW ' +
974
+ `GCLASS_MACHINE_VIEW_${v.mode.toUpperCase()}` +
975
+ (on? ' is-active': ''),
976
+ type: 'button',
977
+ 'aria-pressed': on? 'true': 'false',
978
+ title: t(v.key), 'data-i18n-title': v.key,
979
+ 'aria-label': t(v.key), 'data-i18n-aria-label': v.key}, [
980
+ ['span', {class: 'icon'}, [['i', {class: v.icon}]]]
981
+ ], {
982
+ click: function(evt) {
983
+ evt.stopPropagation();
984
+ gobj_send_event(gobj, "EV_SET_MACHINE_VIEW",
985
+ {mode: v.mode}, gobj);
986
+ }
987
+ }];
988
+ });
989
+
990
+ return ['div', {class: 'GCLASS_MACHINE_SWITCH buttons has-addons mb-0'},
991
+ buttons];
992
+ }
993
+
994
+ /************************************************************
995
+ * What the drawing means. Only the parts that are ON the
996
+ * screen: a legend for a mark nobody drew is noise.
997
+ ************************************************************/
998
+ function build_legend(fsm, current)
999
+ {
1000
+ let items = [];
1001
+
1002
+ items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
1003
+ ['i', {class: 'GCLASS_LEGEND_SWATCH is-none'}],
1004
+ ['span', {i18n: 'not declared in this state'}, t("not declared in this state")]
1005
+ ]]);
1006
+
1007
+ if(current && fsm.states.some((s) => s.name === current)) {
1008
+ items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
1009
+ ['i', {class: 'GCLASS_LEGEND_SWATCH is-current'}],
1010
+ ['span', {i18n: 'current state'}, t("current state")]
1011
+ ]]);
1012
+ }
1013
+
1014
+ if(fsm.unreachable.length) {
1015
+ items.push(['span', {class: 'GCLASS_LEGEND_ITEM'}, [
1016
+ ['i', {class: 'GCLASS_LEGEND_SWATCH is-unreachable'}, "⚠"],
1017
+ ['span', {i18n: 'state with no declared entry'},
1018
+ t("state with no declared entry")]
1019
+ ]]);
1020
+ }
1021
+
1022
+ return ['div', {class: 'GCLASS_LEGEND'}, items];
1023
+ }
1024
+
1025
+ /************************************************************
1026
+ * Event flags, as chips. Only the ones that say something
1027
+ * about the event itself.
1028
+ ************************************************************/
1029
+ function event_flag_chips(flags)
1030
+ {
1031
+ return flags.filter((f) => f.indexOf("OUTPUT") >= 0 || f.indexOf("PUBLIC") >= 0)
1032
+ .map(function(flag) {
1033
+ let cls = 'GCLASS_CHIP GCLASS_EVFLAG';
1034
+ if(flag.indexOf("OUTPUT") >= 0) {
1035
+ cls += ' is-output';
1036
+ } else {
1037
+ cls += ' is-public';
1038
+ }
1039
+ return ['span', {class: cls}, flag.replace("EVF_", "")];
1040
+ });
1041
+ }
1042
+
1043
+ /************************************************************
1044
+ * Scroll the matrix so the lit column is on screen.
1045
+ *
1046
+ * The reader opened this viewer from a gobj that is in one
1047
+ * particular state, and a matrix wide enough to need a
1048
+ * scroller starts at ITS left edge -- which is where that
1049
+ * state usually is not. Only when it is off screen, and only
1050
+ * as far as it takes: a matrix that jumped every render
1051
+ * would fight the reader who scrolled it somewhere else.
1052
+ ************************************************************/
1053
+ function reveal_current_state(gobj)
1054
+ {
1055
+ let priv = gobj.priv;
1056
+ let $zones = priv.$zones;
1057
+ if(!$zones) {
1058
+ return;
1059
+ }
1060
+
1061
+ let $th = $zones.querySelector('.GCLASS_MX_STATE.is-current');
1062
+ if(!$th || typeof $th.closest !== "function") {
1063
+ return;
1064
+ }
1065
+ let $scroll = $th.closest('.GCLASS_SCROLL');
1066
+ if(!$scroll) {
1067
+ return;
1068
+ }
1069
+
1070
+ let cell = $th.getBoundingClientRect();
1071
+ let box = $scroll.getBoundingClientRect();
1072
+ if(cell.right > box.right) {
1073
+ $scroll.scrollLeft += (cell.right - box.right) + 12;
1074
+ } else if(cell.left < box.left) {
1075
+ $scroll.scrollLeft -= (box.left - cell.left) + 12;
1076
+ }
1077
+ }
1078
+
1079
+ /************************************************************
1080
+ * Build the hosted graph child on its mount.
1081
+ *
1082
+ * Here, and never in build_machine: the mount has to be in
1083
+ * the document before the child starts, and the child is
1084
+ * started last for the same reason -- mt_start builds the
1085
+ * G6 graph and measures the canvas.
1086
+ ************************************************************/
1087
+ function build_graph_child(gobj)
1088
+ {
1089
+ let priv = gobj.priv;
1090
+ let $zones = priv.$zones;
1091
+
1092
+ if(!$zones) {
1093
+ return false;
1094
+ }
1095
+
1096
+ let $mount = $zones.querySelector('.GCLASS_GRAPH_MOUNT');
1097
+ if(!$mount) {
1098
+ return false; /* the matrix is showing: nothing to mount */
1099
+ }
1100
+
1101
+ let graph = gobj_create_pure_child(
1102
+ "fsm_" + clean_name(gobj_name(gobj)),
1103
+ "C_YUI_FSM_GRAPH",
1104
+ {
1105
+ fsm: priv.model? priv.model.fsm: null,
1106
+ current_state: gobj_read_str_attr(gobj, "current_state"),
1107
+ },
1108
+ gobj
1109
+ );
1110
+ if(!graph) {
1111
+ log_error(`${gobj_short_name(gobj)}: cannot create the machine graph`);
1112
+ return false;
1113
+ }
1114
+
1115
+ let $box = gobj_read_attr(graph, "$container");
1116
+ if(!$box) {
1117
+ log_error(`${gobj_short_name(gobj)}: the machine graph built no $container`);
1118
+ gobj_destroy(graph);
1119
+ return false;
1120
+ }
1121
+
1122
+ $mount.appendChild($box);
1123
+ priv.graph_gobj = graph;
1124
+ gobj_start(graph);
1125
+ return true;
1126
+ }
1127
+
1128
+ /************************************************************
1129
+ * Destroy the hosted graph child, if any.
1130
+ ************************************************************/
1131
+ function teardown_graph_child(gobj)
1132
+ {
1133
+ let priv = gobj.priv;
1134
+
1135
+ if(priv.graph_gobj) {
1136
+ if(gobj_is_running(priv.graph_gobj)) {
1137
+ gobj_stop(priv.graph_gobj);
1138
+ }
1139
+ gobj_destroy(priv.graph_gobj);
1140
+ priv.graph_gobj = null;
1141
+ }
1142
+ }
1143
+
1144
+ /************************************************************
1145
+ * Apply the current filter to what is drawn.
1146
+ *
1147
+ * Rows carry their own haystack (`data-find`), so this hides
1148
+ * rows and never re-renders: a filter that rebuilt the view
1149
+ * would lose the scroll position on every keystroke.
1150
+ ************************************************************/
1151
+ function apply_search(gobj)
1152
+ {
1153
+ let priv = gobj.priv;
1154
+ let $zones = priv.$zones;
1155
+ if(!$zones) {
1156
+ return;
1157
+ }
1158
+
1159
+ let term = priv.search;
1160
+
1161
+ $zones.querySelectorAll('.GCLASS_ZONE').forEach(function($zone) {
1162
+ let $rows = $zone.querySelectorAll('[data-find]');
1163
+ let visible = 0;
1164
+
1165
+ $rows.forEach(function($row) {
1166
+ let hit = !term || $row.getAttribute('data-find').indexOf(term) >= 0;
1167
+ $row.classList.toggle('is-hidden', !hit);
1168
+ if(hit) {
1169
+ visible++;
1170
+ }
1171
+ });
1172
+
1173
+ /* A zone whose every row is filtered out reads as broken
1174
+ * unless it says what happened. */
1175
+ let $none = $zone.querySelector('.GCLASS_ZONE_NO_MATCH');
1176
+ let show_none = (term && $rows.length > 0 && visible === 0);
1177
+ if(show_none && !$none) {
1178
+ let $body = $zone.querySelector('.GCLASS_ZONE_BODY');
1179
+ if($body) {
1180
+ $body.appendChild(createElement2(
1181
+ ['div', {class: 'GCLASS_ZONE_EMPTY GCLASS_ZONE_NO_MATCH',
1182
+ i18n: 'no matches'}, t("no matches")]));
1183
+ }
1184
+ } else if(!show_none && $none) {
1185
+ $none.parentNode.removeChild($none);
1186
+ }
1187
+ });
1188
+ }
1189
+
1190
+
1191
+
1192
+
1193
+ /***************************
1194
+ * Actions
1195
+ ***************************/
1196
+
1197
+
1198
+
1199
+
1200
+ /************************************************************
1201
+ * EV_SET_DESCRIPTION {description, current_state}
1202
+ ************************************************************/
1203
+ function ac_set_description(gobj, event, kw, src)
1204
+ {
1205
+ let priv = gobj.priv;
1206
+ let description = kw.description;
1207
+
1208
+ if(description === undefined) {
1209
+ log_error(`${GCLASS_NAME}: EV_SET_DESCRIPTION without a description`);
1210
+ return -1;
1211
+ }
1212
+
1213
+ gobj_write_attr(gobj, "description",
1214
+ (description === null)? null: json_deep_copy(description));
1215
+ if(typeof kw.current_state === "string") {
1216
+ gobj_write_attr(gobj, "current_state", kw.current_state);
1217
+ }
1218
+
1219
+ priv.model = gclass_view_model(gobj_read_attr(gobj, "description"));
1220
+ render_view(gobj);
1221
+ return 0;
1222
+ }
1223
+
1224
+ /************************************************************
1225
+ * EV_SET_VIEW_MODE {mode}
1226
+ ************************************************************/
1227
+ function ac_set_view_mode(gobj, event, kw, src)
1228
+ {
1229
+ let mode = kw.mode;
1230
+
1231
+ if(!VIEWS.some((v) => v.mode === mode)) {
1232
+ log_error(`${GCLASS_NAME}: unknown view_mode '${mode}'`);
1233
+ return -1;
1234
+ }
1235
+ if(mode === current_view_mode(gobj)) {
1236
+ return 0;
1237
+ }
1238
+
1239
+ gobj_write_attr(gobj, "view_mode", mode);
1240
+ apply_view_mode(gobj);
1241
+ render_view(gobj);
1242
+ return 0;
1243
+ }
1244
+
1245
+ /************************************************************
1246
+ * EV_TOGGLE_ZONE {zone}
1247
+ ************************************************************/
1248
+ function ac_toggle_zone(gobj, event, kw, src)
1249
+ {
1250
+ let priv = gobj.priv;
1251
+ let zone = kw.zone;
1252
+
1253
+ if(!ZONES.some((z) => z.id === zone)) {
1254
+ log_error(`${GCLASS_NAME}: unknown zone '${zone}'`);
1255
+ return -1;
1256
+ }
1257
+
1258
+ let collapsed = !priv.collapsed.has(zone);
1259
+ if(collapsed) {
1260
+ priv.collapsed.add(zone);
1261
+ } else {
1262
+ priv.collapsed.delete(zone);
1263
+ }
1264
+
1265
+ /*
1266
+ * The DOM is folded in place, never re-rendered: a rebuild would
1267
+ * throw away the hosted graph and its camera every time somebody
1268
+ * folded an unrelated zone.
1269
+ */
1270
+ let $zone = priv.$zones?
1271
+ priv.$zones.querySelector(`.GCLASS_ZONE[data-zone="${zone}"]`): null;
1272
+ if(!$zone) {
1273
+ log_error(`${GCLASS_NAME}: zone '${zone}' is not drawn`);
1274
+ return -1;
1275
+ }
1276
+
1277
+ $zone.classList.toggle('is-collapsed', collapsed);
1278
+
1279
+ let $body = $zone.querySelector('.GCLASS_ZONE_BODY');
1280
+ if($body) {
1281
+ $body.classList.toggle('is-hidden', collapsed);
1282
+ }
1283
+
1284
+ let $toggle = $zone.querySelector('.GCLASS_ZONE_TOGGLE');
1285
+ if($toggle) {
1286
+ $toggle.setAttribute("aria-expanded", collapsed? "false": "true");
1287
+ }
1288
+
1289
+ let $arrow = $zone.querySelector('.GCLASS_ZONE_ARROW i');
1290
+ if($arrow) {
1291
+ $arrow.className = collapsed? 'yi-chevron-right': 'yi-chevron-down';
1292
+ }
1293
+
1294
+ /* A graph built inside a folded zone measures 0x0 and keeps that
1295
+ * size for good, so it is built when the zone opens and dropped
1296
+ * when it closes. */
1297
+ if(zone === "machine" && priv.machine_view === "graph") {
1298
+ if(collapsed) {
1299
+ teardown_graph_child(gobj);
1300
+ } else if(!priv.graph_gobj) {
1301
+ build_graph_child(gobj);
1302
+ }
1303
+ }
1304
+
1305
+ return 0;
1306
+ }
1307
+
1308
+ /************************************************************
1309
+ * EV_SET_MACHINE_VIEW {mode}
1310
+ ************************************************************/
1311
+ function ac_set_machine_view(gobj, event, kw, src)
1312
+ {
1313
+ let priv = gobj.priv;
1314
+ let mode = kw.mode;
1315
+
1316
+ if(!MACHINE_VIEWS.some((v) => v.mode === mode)) {
1317
+ log_error(`${GCLASS_NAME}: unknown machine view '${mode}'`);
1318
+ return -1;
1319
+ }
1320
+ if(mode === priv.machine_view) {
1321
+ return 0;
1322
+ }
1323
+
1324
+ priv.machine_view = mode;
1325
+ render_zones(gobj);
1326
+ return 0;
1327
+ }
1328
+
1329
+ /************************************************************
1330
+ * EV_SEARCH {text}
1331
+ ************************************************************/
1332
+ function ac_search(gobj, event, kw, src)
1333
+ {
1334
+ let priv = gobj.priv;
1335
+ priv.search = String(kw.text || "").trim().toLowerCase();
1336
+ apply_search(gobj);
1337
+ return 0;
1338
+ }
1339
+
1340
+ /************************************************************
1341
+ * EV_COPY_ALL -- the description, as the backend answered it.
1342
+ ************************************************************/
1343
+ function ac_copy_all(gobj, event, kw, src)
1344
+ {
1345
+ let description = gobj_read_attr(gobj, "description");
1346
+ let $container = gobj_read_attr(gobj, "$container");
1347
+
1348
+ if(description === null || description === undefined) {
1349
+ return 0;
1350
+ }
1351
+
1352
+ yui_copy_json(description);
1353
+
1354
+ if($container) {
1355
+ let $btn = $container.querySelector('.EV_COPY_ALL');
1356
+ if($btn) {
1357
+ yui_button_mark_done($btn, t("copied"));
1358
+ }
1359
+ }
1360
+ return 0;
1361
+ }
1362
+
1363
+ /************************************************************
1364
+ * EV_LANGUAGE_CHANGED -- re-translate chrome + re-render
1365
+ *
1366
+ * The tables are BUILT with t(): their headers and their
1367
+ * empty lines carry keys, but the matrix draws state and
1368
+ * event names that are not translatable and legend text that
1369
+ * is. Re-rendering is the only way both end up in the same
1370
+ * language.
1371
+ ************************************************************/
1372
+ function ac_language_changed(gobj, event, kw, src)
1373
+ {
1374
+ let priv = gobj.priv;
1375
+ let $container = gobj_read_attr(gobj, "$container");
1376
+
1377
+ if($container) {
1378
+ refresh_language($container, t);
1379
+ if(priv.$search) {
1380
+ priv.$search.setAttribute("placeholder", t("search"));
1381
+ }
1382
+ apply_view_mode(gobj);
1383
+ }
1384
+ render_view(gobj);
1385
+ return 0;
1386
+ }
1387
+
1388
+ /************************************************************
1389
+ * EV_REFRESH
1390
+ ************************************************************/
1391
+ function ac_refresh(gobj, event, kw, src)
1392
+ {
1393
+ render_view(gobj);
1394
+ return 0;
1395
+ }
1396
+
1397
+ /************************************************************
1398
+ * EV_SHOW / EV_HIDE -- host visibility
1399
+ ************************************************************/
1400
+ function ac_show(gobj, event, kw, src)
1401
+ {
1402
+ let $container = gobj_read_attr(gobj, "$container");
1403
+ if($container) {
1404
+ $container.classList.remove('is-hidden');
1405
+ }
1406
+ return 0;
1407
+ }
1408
+
1409
+ function ac_hide(gobj, event, kw, src)
1410
+ {
1411
+ let $container = gobj_read_attr(gobj, "$container");
1412
+ if($container) {
1413
+ $container.classList.add('is-hidden');
1414
+ }
1415
+ return 0;
1416
+ }
1417
+
1418
+
1419
+
1420
+
1421
+ /***************************
1422
+ * FSM
1423
+ ***************************/
1424
+
1425
+
1426
+
1427
+
1428
+ /*---------------------------------------------*
1429
+ * Global methods table
1430
+ *---------------------------------------------*/
1431
+ const gmt = {
1432
+ mt_create: mt_create,
1433
+ mt_start: mt_start,
1434
+ mt_stop: mt_stop,
1435
+ mt_destroy: mt_destroy,
1436
+ };
1437
+
1438
+ /***************************************************************
1439
+ * Create the GClass
1440
+ ***************************************************************/
1441
+ function create_gclass(gclass_name)
1442
+ {
1443
+ if(__gclass__) {
1444
+ log_error(`GClass ALREADY created: ${gclass_name}`);
1445
+ return -1;
1446
+ }
1447
+
1448
+ /*---------------------------------------------*
1449
+ * States
1450
+ *---------------------------------------------*/
1451
+ const states = [
1452
+ ["ST_IDLE", [
1453
+ ["EV_SET_DESCRIPTION", ac_set_description, null],
1454
+ ["EV_SET_VIEW_MODE", ac_set_view_mode, null],
1455
+ ["EV_TOGGLE_ZONE", ac_toggle_zone, null],
1456
+ ["EV_SET_MACHINE_VIEW", ac_set_machine_view, null],
1457
+ ["EV_SEARCH", ac_search, null],
1458
+ ["EV_COPY_ALL", ac_copy_all, null],
1459
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
1460
+ ["EV_REFRESH", ac_refresh, null],
1461
+ ["EV_SHOW", ac_show, null],
1462
+ ["EV_HIDE", ac_hide, null]
1463
+ ]]
1464
+ ];
1465
+
1466
+ /*---------------------------------------------*
1467
+ * Events
1468
+ *---------------------------------------------*/
1469
+ const event_types = [
1470
+ ["EV_SET_DESCRIPTION", 0],
1471
+ ["EV_SET_VIEW_MODE", 0],
1472
+ ["EV_TOGGLE_ZONE", 0],
1473
+ ["EV_SET_MACHINE_VIEW", 0],
1474
+ ["EV_SEARCH", 0],
1475
+ ["EV_COPY_ALL", 0],
1476
+ ["EV_LANGUAGE_CHANGED", 0],
1477
+ ["EV_REFRESH", 0],
1478
+ ["EV_SHOW", 0],
1479
+ ["EV_HIDE", 0]
1480
+ ];
1481
+
1482
+ __gclass__ = gclass_create(
1483
+ gclass_name,
1484
+ event_types,
1485
+ states,
1486
+ gmt,
1487
+ 0, // lmt,
1488
+ attrs_table,
1489
+ PRIVATE_DATA,
1490
+ 0, // authz_table,
1491
+ 0, // command_table,
1492
+ 0, // s_user_trace_level
1493
+ 0 // gclass_flag
1494
+ );
1495
+
1496
+ if(!__gclass__) {
1497
+ return -1;
1498
+ }
1499
+
1500
+ return 0;
1501
+ }
1502
+
1503
+ /***************************************************************
1504
+ * Register GClass
1505
+ ***************************************************************/
1506
+ function register_c_yui_gclass()
1507
+ {
1508
+ /* Idempotent: yui_gclass_view.js registers this gclass for any
1509
+ * host that offers a "view gclass" control, so an app that also
1510
+ * registers it explicitly must not trip "GClass ALREADY created". */
1511
+ if(gclass_find_by_name(GCLASS_NAME, false)) {
1512
+ return 0;
1513
+ }
1514
+ /* The machine's graph view hosts a C_YUI_FSM_GRAPH child: make
1515
+ * sure its gclass exists even if the app never registered it. */
1516
+ if(!gclass_find_by_name("C_YUI_FSM_GRAPH", false)) {
1517
+ register_c_yui_fsm_graph();
1518
+ }
1519
+ return create_gclass(GCLASS_NAME);
1520
+ }
1521
+
1522
+ export { register_c_yui_gclass };