@yuneta/gobj-ui 7.23.141 → 7.23.143

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/index.js CHANGED
@@ -23,6 +23,7 @@ export { register_c_yui_map } from "./src/c_yui_map.js";
23
23
  export { register_c_yui_uplot } from "./src/c_yui_uplot.js";
24
24
  export { register_c_yui_json_graph } from "./src/c_yui_json_graph.js";
25
25
  export { register_c_yui_json } from "./src/c_yui_json.js";
26
+ export { register_c_yui_json_pad } from "./src/c_yui_json_pad.js";
26
27
  export { register_c_yui_gclass } from "./src/c_yui_gclass.js";
27
28
  export { register_c_yui_fsm_graph } from "./src/c_yui_fsm_graph.js";
28
29
  export { register_c_yui_gobj_tree_js } from "./src/c_yui_gobj_tree_js.js";
@@ -129,6 +130,7 @@ export { yui_theme_now, yui_is_dark, yui_watch_theme } from "./src/yui_theme.js"
129
130
  export { attach_clear, refresh_clear } from "./src/yui_inputs.js";
130
131
  export { info_traffic, setup_dev, build_dev_panel, apply_dev_traces, dev_window_was_open } from "./src/yui_dev.js";
131
132
  export { setup_frontend_view } from "./src/yui_frontend_view.js";
133
+ export { setup_json_pad } from "./src/yui_json_pad.js";
132
134
 
133
135
  /*
134
136
  * CSS - import these in your main entry point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.141",
3
+ "version": "7.23.143",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -122,6 +122,7 @@ import {
122
122
  fold_open_levels,
123
123
  fold_close_levels,
124
124
  fold_level_clamp,
125
+ fold_is_hierarchical,
125
126
  fold_toggle,
126
127
  fold_show_more,
127
128
  fold_reveal,
@@ -301,14 +302,14 @@ const PORT_LINE_WIDTH = 2;
301
302
  const NODE_MODES = ['expanded', 'compact', 'shape'];
302
303
 
303
304
  const PILL_SIZE = {
304
- hierarchical: [200, 30],
305
+ entity: [200, 30],
305
306
  extended: [180, 28],
306
307
  child: [160, 26],
307
308
  };
308
309
  const PORT_R_PILL = 6;
309
310
 
310
311
  const SHAPE_SIZE = {
311
- hierarchical: 32,
312
+ entity: 32,
312
313
  extended: 28,
313
314
  child: 22,
314
315
  };
@@ -436,7 +437,7 @@ SDATA(data_type_t.DTP_STRING, "hook_port_position", 0, "bottom", "Hook po
436
437
  SDATA(data_type_t.DTP_STRING, "fkey_port_position", 0, "top", "Fkey port position"),
437
438
 
438
439
  /*---------------- Folding ----------------*/
439
- SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels open when a treedb loads, until the reader steps (`fold_level`): 1 = the roots alone, 2 = the roots and their children. Every open hook shows its first page"),
440
+ SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 1, "Levels open when a treedb loads, until the reader steps (`fold_level`): 1 = the roots alone (the default), 2 = the roots and their children. Every open hook shows its first page"),
440
441
  SDATA(data_type_t.DTP_INTEGER, "fold_level", 0, 0, "The level of the MAIN tree the toolbar's stepper stands on: 1 = its roots, N = N-1 levels under them; 0 = `expand_depth`. The host persists it and sends EV_SET_FOLD_LEVEL; it is published back in EV_LEGEND_STATE (`fold`) with the levels the tree has"),
441
442
  SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page; the `+N` chip after the page opens the next one"),
442
443
  SDATA(data_type_t.DTP_LIST, "hidden_topics", 0, "[]", "Topics left out of the tree: no card, no pill counts them, no edge reaches them"),
@@ -545,7 +546,7 @@ let PRIVATE_DATA = {
545
546
  _nodes_placed: 0, // ...of which carry a saved position
546
547
 
547
548
  /*---------------- folding ----------------*/
548
- expand_depth: 2,
549
+ expand_depth: 1,
549
550
  fold_level: 0,
550
551
  fold_page_size: 24,
551
552
  hidden_topics: null,
@@ -1859,7 +1860,12 @@ function configure_behaviour(gobj)
1859
1860
  }
1860
1861
 
1861
1862
  /************************************************************
1862
- * Count hooks and fkeys in topic desc, classify node type
1863
+ * Count hooks and fkeys in topic desc, classify node type:
1864
+ * `child` (no hooks), `extended` (hooks, no fkeys), `entity`
1865
+ * (both). It sizes the card and nothing else. It used to be
1866
+ * called `hierarchical`, and that word means something else now:
1867
+ * a topic hooked to ITSELF, the only kind a tree can hang from
1868
+ * (treedb_fold_model).
1863
1869
  ************************************************************/
1864
1870
  function calculate_hooks_fkeys_counter(desc)
1865
1871
  {
@@ -1885,7 +1891,7 @@ function calculate_hooks_fkeys_counter(desc)
1885
1891
  } else if(desc.fkeys_counter === 0) {
1886
1892
  desc.node_treedb_type = 'extended';
1887
1893
  } else {
1888
- desc.node_treedb_type = 'hierarchical';
1894
+ desc.node_treedb_type = 'entity';
1889
1895
  }
1890
1896
  }
1891
1897
 
@@ -2306,7 +2312,7 @@ function card_shape_of(gobj, desc, record, geometry, pills_html)
2306
2312
  desc.topic_name, true, record.id, false, false, false, pills_html
2307
2313
  );
2308
2314
  } else {
2309
- // Hierarchical entity (ROOT / container): largest tier.
2315
+ // Entity, hooks and fkeys (ROOT / container): largest tier.
2310
2316
  style.innerHTML = build_node_innerHTML(
2311
2317
  paint.fill, priv.theme, record.icon, label,
2312
2318
  desc.topic_name, false, record.id, false, false, false, pills_html
@@ -2409,7 +2415,7 @@ function pill_shape_of(gobj, desc, record, geometry)
2409
2415
  {
2410
2416
  let priv = gobj.priv;
2411
2417
  let tier = desc.node_treedb_type;
2412
- let size = PILL_SIZE[tier] || PILL_SIZE.hierarchical;
2418
+ let size = PILL_SIZE[tier] || PILL_SIZE.entity;
2413
2419
  let ports = build_ports(gobj, desc);
2414
2420
  let paint = node_paint_of(gobj, desc, geometry);
2415
2421
 
@@ -2472,7 +2478,7 @@ function figure_shape_of(gobj, desc, record, geometry, flags)
2472
2478
  let dark = (priv.theme === "dark");
2473
2479
  let f = flags || {};
2474
2480
  let tier = desc.node_treedb_type;
2475
- let side = SHAPE_SIZE[tier] || SHAPE_SIZE.hierarchical;
2481
+ let side = SHAPE_SIZE[tier] || SHAPE_SIZE.entity;
2476
2482
  let labels = gobj_read_bool_attr(gobj, "node_labels");
2477
2483
  let figure = node_figure_of(gobj, desc, geometry || {});
2478
2484
  let paint = node_paint_of(gobj, desc, geometry);
@@ -7042,7 +7048,7 @@ function show_node_detail_popover(gobj, node_id)
7042
7048
  }
7043
7049
 
7044
7050
  /************************************************************
7045
- * Re-render every HTML (hierarchical) node so its card adopts
7051
+ * Re-render every HTML (entity) node so its card adopts
7046
7052
  * the given theme: entity cards and leaf chips (baked innerHTML,
7047
7053
  * not re-themed by setTheme()) are regenerated; structural
7048
7054
  * junction diamonds get their neutral fill/stroke re-themed.
@@ -7258,7 +7264,7 @@ function refresh_minimap(gobj)
7258
7264
 
7259
7265
  /************************************************************
7260
7266
  * The innerHTML of ONE node, in the given theme and highlight state.
7261
- * The three treedb tiers (hierarchical / extended / child) each have
7267
+ * The three treedb tiers (entity / extended / child) each have
7262
7268
  * their own card, and both the theme refresh and the highlight need the
7263
7269
  * same three-way choice — it lived inline in the theme refresh and is
7264
7270
  * shared now. Returns null for a node that carries no desc.
@@ -7295,7 +7301,7 @@ function node_innerHTML_of(nd, theme, highlight, selected, anchored, pills_html)
7295
7301
  desc.topic_name, true, record.id, highlight, selected, anchored,
7296
7302
  pills_html
7297
7303
  );
7298
- case 'hierarchical':
7304
+ case 'entity':
7299
7305
  return build_node_innerHTML(
7300
7306
  fill, theme, record.icon, label,
7301
7307
  desc.topic_name, false, record.id, highlight, selected, anchored,
@@ -7569,7 +7575,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
7569
7575
  }
7570
7576
 
7571
7577
  /************************************************************
7572
- * Build innerHTML for hierarchical (HTML) nodes.
7578
+ * Build innerHTML for entity (HTML) nodes.
7573
7579
  *
7574
7580
  * Card style modelled on the documentation schema graphs
7575
7581
  * (docs gen_treedb_graphs.py): rounded card, soft tint of the
@@ -9538,12 +9544,16 @@ function publish_legend_state(gobj, visible)
9538
9544
  loose_shown: model.loose_shown.has(topic_name),
9539
9545
  hidden: model.hidden.has(topic_name),
9540
9546
  linked: model.linked.has(topic_name),
9547
+ hierarchical: fold_is_hierarchical(priv.descs, topic_name),
9541
9548
  });
9542
9549
  }
9543
9550
 
9544
9551
  gobj_publish_event(gobj, "EV_LEGEND_STATE", {
9545
9552
  main_topic: model.main_topic,
9546
- main_chosen: !!gobj_read_str_attr(gobj, "main_topic"),
9553
+ /* CHOSEN only when the reader's pick is the one in force: a
9554
+ * pick the rule refuses (not hierarchical) is ignored. */
9555
+ main_chosen: !!model.main_topic &&
9556
+ gobj_read_str_attr(gobj, "main_topic") === model.main_topic,
9547
9557
  topics: topics,
9548
9558
  /* The toolbar's stepper: the tree it walks, where it stands,
9549
9559
  * and how deep it goes. `levels` 0 = nothing to step through. */
@@ -0,0 +1,454 @@
1
+ /***********************************************************************
2
+ * c_yui_json_pad.js
3
+ *
4
+ * C_YUI_JSON_PAD -- a pad to paste JSON from OUTSIDE and read it
5
+ * with the library's own viewer.
6
+ *
7
+ * The viewer (C_YUI_JSON: the lazy tree, the raw text and the
8
+ * graph) only ever showed what an app fetched. A JSON that came
9
+ * from anywhere else -- a log line, a config file, an answer copied
10
+ * from a terminal -- had to be read in whatever the operator had
11
+ * at hand. This is the blank page for it: paste, and read it the
12
+ * way every other document of the app is read.
13
+ *
14
+ * DOM (UPPER_SNAKE logical classes):
15
+ *
16
+ * JSON_PAD the root
17
+ * JSON_PAD_BAR the text area and its two buttons
18
+ * JSON_PAD_INPUT where the text goes
19
+ * JSON_PAD_VIEW read what is in the text area
20
+ * JSON_PAD_CLEAR empty the pad
21
+ * JSON_PAD_ERROR "invalid JSON" and the parser's reason
22
+ * JSON_PAD_BODY the C_YUI_JSON child
23
+ *
24
+ * Every way in is an event:
25
+ *
26
+ * EV_PASTE {text} a paste ANYWHERE in the pad replaces the
27
+ * document and shows it at once -- there
28
+ * is one document here, not a text to edit
29
+ * EV_VIEW the text area as it is (typed, or edited
30
+ * after a paste); also Ctrl+Enter
31
+ * EV_CLEAR back to the blank pad
32
+ *
33
+ * FSM: ST_EMPTY (nothing shown) and ST_SHOWING. EV_EXPAND_PATH,
34
+ * which the viewer publishes when a `__collapsed__` sentinel is
35
+ * opened, only exists in ST_SHOWING, and is answered with
36
+ * EV_SUBTREE_ERROR: a pasted document has no backend to fetch the
37
+ * rest from -- the source truncated it, and the pad says so.
38
+ *
39
+ * Hosted by setup_json_pad() (yui_json_pad.js) in a C_YUI_WINDOW;
40
+ * any host can mount `$container` itself.
41
+ *
42
+ * Copyright (c) 2026, ArtGins.
43
+ * All Rights Reserved.
44
+ ***********************************************************************/
45
+ import {
46
+ SDATA,
47
+ SDATA_END,
48
+ data_type_t,
49
+ gclass_create,
50
+ gclass_find_by_name,
51
+ log_error,
52
+ log_warning,
53
+ gobj_create_pure_child,
54
+ gobj_read_pointer_attr,
55
+ gobj_read_attr,
56
+ gobj_write_attr,
57
+ gobj_subscribe_event,
58
+ gobj_send_event,
59
+ gobj_change_state,
60
+ gobj_start,
61
+ gobj_stop,
62
+ gobj_is_running,
63
+ gobj_short_name,
64
+ gobj_parent,
65
+ createElement2,
66
+ } from "@yuneta/gobj-js";
67
+
68
+ import {register_c_yui_json} from "./c_yui_json.js";
69
+ import {yui_toolbar_icon} from "./yui_toolbar.js";
70
+
71
+ import {t} from "i18next";
72
+
73
+ /***************************************************************
74
+ * Constants
75
+ ***************************************************************/
76
+ const GCLASS_NAME = "C_YUI_JSON_PAD";
77
+
78
+ /***************************************************************
79
+ * Data
80
+ ***************************************************************/
81
+ const attrs_table = [
82
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
83
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the host"),
84
+ SDATA_END()
85
+ ];
86
+
87
+ let PRIVATE_DATA = {
88
+ $input: null, /* the text area */
89
+ $error: null, /* the "invalid JSON" line */
90
+ $reason: null, /* ...and the parser's reason inside it */
91
+ gobj_json: null, /* the C_YUI_JSON child */
92
+ };
93
+
94
+ let __gclass__ = null;
95
+
96
+
97
+
98
+
99
+ /******************************
100
+ * Framework Methods
101
+ ******************************/
102
+
103
+
104
+
105
+
106
+ /***************************************************************
107
+ * Framework Method: Create
108
+ ***************************************************************/
109
+ function mt_create(gobj)
110
+ {
111
+ let priv = gobj.priv;
112
+
113
+ /*
114
+ * CHILD subscription model
115
+ */
116
+ let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
117
+ if(!subscriber) {
118
+ subscriber = gobj_parent(gobj);
119
+ }
120
+ gobj_subscribe_event(gobj, null, {}, subscriber);
121
+
122
+ build_ui(gobj);
123
+
124
+ /*
125
+ * The viewer, as a pure child: it publishes EV_EXPAND_PATH to its
126
+ * parent (this gobj), which declares it in ST_SHOWING.
127
+ */
128
+ priv.gobj_json = gobj_create_pure_child("json_pad_view", "C_YUI_JSON", {}, gobj);
129
+ let $view = gobj_read_attr(priv.gobj_json, "$container");
130
+ if(!$view) {
131
+ log_error(`${gobj_short_name(gobj)}: C_YUI_JSON without $container`);
132
+ return;
133
+ }
134
+ gobj_read_attr(gobj, "$container").querySelector(".JSON_PAD_BODY").appendChild($view);
135
+ }
136
+
137
+ /***************************************************************
138
+ * Framework Method: Start
139
+ ***************************************************************/
140
+ function mt_start(gobj)
141
+ {
142
+ let priv = gobj.priv;
143
+
144
+ if(priv.gobj_json) {
145
+ gobj_start(priv.gobj_json);
146
+ }
147
+ /* A blank pad is waiting for a paste: the caret goes where it lands. */
148
+ if(priv.$input) {
149
+ priv.$input.focus();
150
+ }
151
+ }
152
+
153
+ /***************************************************************
154
+ * Framework Method: Stop
155
+ ***************************************************************/
156
+ function mt_stop(gobj)
157
+ {
158
+ let priv = gobj.priv;
159
+
160
+ if(priv.gobj_json && gobj_is_running(priv.gobj_json)) {
161
+ gobj_stop(priv.gobj_json);
162
+ }
163
+ }
164
+
165
+ /***************************************************************
166
+ * Framework Method: Destroy
167
+ ***************************************************************/
168
+ function mt_destroy(gobj)
169
+ {
170
+ }
171
+
172
+
173
+
174
+
175
+ /***************************
176
+ * Local Methods
177
+ ***************************/
178
+
179
+
180
+
181
+
182
+ /***************************************************************
183
+ * The pad: the text area and its buttons, the error line, and
184
+ * the body the viewer is mounted in.
185
+ ***************************************************************/
186
+ function build_ui(gobj)
187
+ {
188
+ let priv = gobj.priv;
189
+
190
+ let button = (cls, icon, key, event_name) => {
191
+ return ['button', {class: `${cls} button`, type: 'button',
192
+ title: t(key), 'data-i18n-title': key,
193
+ 'aria-label': t(key), 'data-i18n-aria-label': key},
194
+ [
195
+ yui_toolbar_icon(icon),
196
+ ['span', {style: 'padding-left:5px;', i18n: key}, key]
197
+ ],
198
+ {
199
+ click: (evt) => {
200
+ evt.stopPropagation();
201
+ gobj_send_event(gobj, event_name, {}, gobj);
202
+ }
203
+ }
204
+ ];
205
+ };
206
+
207
+ priv.$input = createElement2(
208
+ ['textarea', {class: 'JSON_PAD_INPUT textarea', rows: 3, spellcheck: 'false',
209
+ placeholder: t('paste json here'), 'data-i18n-placeholder': 'paste json here',
210
+ title: t('paste json here'), 'data-i18n-title': 'paste json here',
211
+ 'aria-label': t('paste json here'), 'data-i18n-aria-label': 'paste json here',
212
+ style: 'font-family:monospace; resize:vertical; min-height:4.5em;'}, '', {
213
+ keydown: (evt) => {
214
+ if((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter') {
215
+ evt.preventDefault();
216
+ gobj_send_event(gobj, "EV_VIEW", {}, gobj);
217
+ }
218
+ }
219
+ }]
220
+ );
221
+
222
+ priv.$reason = createElement2(
223
+ ['span', {class: 'JSON_PAD_ERROR_REASON', style: 'margin-left:.5em; font-family:monospace;'}, '']
224
+ );
225
+ priv.$error = createElement2(
226
+ ['div', {class: 'JSON_PAD_ERROR is-hidden has-text-danger is-size-7', role: 'alert'}, [
227
+ ['span', {class: 'JSON_PAD_ERROR_LABEL', style: 'font-weight:600;', i18n: 'invalid json'},
228
+ 'invalid json'],
229
+ priv.$reason
230
+ ]]
231
+ );
232
+
233
+ let $container = createElement2(
234
+ ['div', {class: 'C_YUI_JSON_PAD JSON_PAD',
235
+ style: 'height:100%; display:flex; flex-direction:column; gap:.5rem; padding:.5rem;'}, [
236
+ ['div', {class: 'JSON_PAD_BAR',
237
+ style: 'display:flex; gap:.5rem; align-items:flex-start;'}, [
238
+ ['div', {style: 'flex:1 1 auto; min-width:0;'}, [priv.$input]],
239
+ ['div', {style: 'flex:0 0 auto; display:flex; flex-direction:column; gap:.5rem;'}, [
240
+ button('JSON_PAD_VIEW', 'yi-eye', 'view', 'EV_VIEW'),
241
+ button('JSON_PAD_CLEAR', 'yi-broom', 'clear', 'EV_CLEAR'),
242
+ ]]
243
+ ]],
244
+ priv.$error,
245
+ ['div', {class: 'JSON_PAD_BODY',
246
+ style: 'flex:1 1 auto; min-height:0; display:flex; flex-direction:column;'}, []]
247
+ ], {
248
+ /* A paste anywhere in the pad is the document: it replaces
249
+ * what the text area held instead of landing at the caret. */
250
+ paste: (evt) => {
251
+ let text = evt.clipboardData? evt.clipboardData.getData("text") : "";
252
+ if(!text) {
253
+ return; /* an image or nothing: the browser's business */
254
+ }
255
+ evt.preventDefault();
256
+ gobj_send_event(gobj, "EV_PASTE", {text: text}, gobj);
257
+ }
258
+ }]
259
+ );
260
+
261
+ gobj_write_attr(gobj, "$container", $container);
262
+ }
263
+
264
+ /***************************************************************
265
+ * Read `text` as JSON and show it, or say why it is not.
266
+ ***************************************************************/
267
+ function show_text(gobj, text)
268
+ {
269
+ let priv = gobj.priv;
270
+ let src = String(text || "").trim();
271
+
272
+ if(!src) {
273
+ clear_pad(gobj, false);
274
+ return 0;
275
+ }
276
+
277
+ let doc;
278
+ try {
279
+ doc = JSON.parse(src);
280
+ } catch(e) {
281
+ /* The reader's text, not a fault of the app: said on the pad, and
282
+ * a warning for whoever reads the console. */
283
+ log_warning(`${gobj_short_name(gobj)}: the pasted text is not JSON: ${e.message}`);
284
+ priv.$reason.textContent = e.message;
285
+ priv.$error.classList.remove('is-hidden');
286
+ return -1;
287
+ }
288
+
289
+ priv.$error.classList.add('is-hidden');
290
+ priv.$reason.textContent = "";
291
+ gobj_send_event(priv.gobj_json, "EV_SET_JSON", {json: doc}, gobj);
292
+ gobj_change_state(gobj, "ST_SHOWING");
293
+ return 0;
294
+ }
295
+
296
+ /***************************************************************
297
+ * Back to the blank pad; `with_text` empties the text area too.
298
+ ***************************************************************/
299
+ function clear_pad(gobj, with_text)
300
+ {
301
+ let priv = gobj.priv;
302
+
303
+ if(with_text) {
304
+ priv.$input.value = "";
305
+ }
306
+ priv.$error.classList.add('is-hidden');
307
+ priv.$reason.textContent = "";
308
+ gobj_send_event(priv.gobj_json, "EV_SET_JSON", {json: null}, gobj);
309
+ gobj_change_state(gobj, "ST_EMPTY");
310
+ }
311
+
312
+
313
+
314
+
315
+ /***************************
316
+ * Actions
317
+ ***************************/
318
+
319
+
320
+
321
+
322
+ /***************************************************************
323
+ * A paste: it becomes the text area and the document at once.
324
+ ***************************************************************/
325
+ function ac_paste(gobj, event, kw, src)
326
+ {
327
+ let priv = gobj.priv;
328
+ let text = (kw && typeof kw.text === "string")? kw.text : "";
329
+
330
+ priv.$input.value = text;
331
+ return show_text(gobj, text);
332
+ }
333
+
334
+ /***************************************************************
335
+ * The text area as it is now.
336
+ ***************************************************************/
337
+ function ac_view(gobj, event, kw, src)
338
+ {
339
+ let priv = gobj.priv;
340
+ return show_text(gobj, priv.$input.value);
341
+ }
342
+
343
+ /***************************************************************
344
+ * The blank pad again.
345
+ ***************************************************************/
346
+ function ac_clear(gobj, event, kw, src)
347
+ {
348
+ let priv = gobj.priv;
349
+
350
+ clear_pad(gobj, true);
351
+ priv.$input.focus();
352
+ return 0;
353
+ }
354
+
355
+ /***************************************************************
356
+ * The viewer asks for a `__collapsed__` subtree. A pasted
357
+ * document has no backend to ask for it: the SOURCE truncated
358
+ * it, and that is the answer.
359
+ ***************************************************************/
360
+ function ac_expand_path(gobj, event, kw, src)
361
+ {
362
+ let priv = gobj.priv;
363
+
364
+ gobj_send_event(priv.gobj_json, "EV_SUBTREE_ERROR", {
365
+ path: (kw && kw.path) || "",
366
+ error: t("collapsed in the source")
367
+ }, gobj);
368
+ return 0;
369
+ }
370
+
371
+ /***************************************************************
372
+ * FSM
373
+ ***************************************************************/
374
+ /*---------------------------------------------*
375
+ * Global methods table
376
+ *---------------------------------------------*/
377
+ const gmt = {
378
+ mt_create: mt_create,
379
+ mt_start: mt_start,
380
+ mt_stop: mt_stop,
381
+ mt_destroy: mt_destroy
382
+ };
383
+
384
+ /***************************************************************
385
+ * Create the GClass
386
+ ***************************************************************/
387
+ function create_gclass(gclass_name)
388
+ {
389
+ if(__gclass__) {
390
+ log_error(`GClass ALREADY created: ${gclass_name}`);
391
+ return -1;
392
+ }
393
+
394
+ /*---------------------------------------------*
395
+ * States
396
+ *---------------------------------------------*/
397
+ const states = [
398
+ ["ST_EMPTY", [
399
+ ["EV_PASTE", ac_paste, null],
400
+ ["EV_VIEW", ac_view, null],
401
+ ["EV_CLEAR", ac_clear, null],
402
+ ]],
403
+ ["ST_SHOWING", [
404
+ ["EV_PASTE", ac_paste, null],
405
+ ["EV_VIEW", ac_view, null],
406
+ ["EV_CLEAR", ac_clear, null],
407
+ ["EV_EXPAND_PATH", ac_expand_path, null],
408
+ ]]
409
+ ];
410
+
411
+ /*---------------------------------------------*
412
+ * Events
413
+ *---------------------------------------------*/
414
+ const event_types = [
415
+ ["EV_PASTE", 0],
416
+ ["EV_VIEW", 0],
417
+ ["EV_CLEAR", 0],
418
+ ["EV_EXPAND_PATH", 0],
419
+ ];
420
+
421
+ __gclass__ = gclass_create(
422
+ gclass_name,
423
+ event_types,
424
+ states,
425
+ gmt,
426
+ 0, // lmt,
427
+ attrs_table,
428
+ PRIVATE_DATA,
429
+ 0, // authz_table,
430
+ 0, // command_table,
431
+ 0, // s_user_trace_level
432
+ 0 // gclass_flag
433
+ );
434
+
435
+ if(!__gclass__) {
436
+ return -1;
437
+ }
438
+
439
+ return 0;
440
+ }
441
+
442
+ /***************************************************************
443
+ * Register GClass
444
+ ***************************************************************/
445
+ function register_c_yui_json_pad()
446
+ {
447
+ /* the viewer it hosts, if the app did not register it */
448
+ if(!gclass_find_by_name("C_YUI_JSON", false)) {
449
+ register_c_yui_json();
450
+ }
451
+ return create_gclass(GCLASS_NAME);
452
+ }
453
+
454
+ export { register_c_yui_json_pad };
@@ -1061,6 +1061,13 @@ function render_topics(gobj, $body)
1061
1061
  ? ["span", {class: "SCHEMA_TOPIC_SYSTEM tag ml-2",
1062
1062
  i18n: "system"}, t("system")]
1063
1063
  : ["span", {}, ""],
1064
+ /* The schema's mark of the topic the tree hangs from: the
1065
+ * same gold star the treedb graph's legend wears. */
1066
+ topic.main_topic
1067
+ ? ["span", {class: "SCHEMA_TOPIC_MAIN ml-2",
1068
+ style: "color:#d4a017;",
1069
+ title: t("main topic"), "data-i18n-title": "main topic"}, "★"]
1070
+ : ["span", {}, ""],
1064
1071
  ambiguous
1065
1072
  ? ["div", {class: "SCHEMA_TOPIC_ID is-size-7 yui-text-quiet"},
1066
1073
  [["code", {}, `${topic.id}`]]]
@@ -1898,6 +1905,14 @@ function open_topic_form(gobj, treedb, topic)
1898
1905
  ["span", {class: "ml-1", i18n: "system topic"}, t("system topic")]
1899
1906
  ]]
1900
1907
  ]],
1908
+ /* A LABEL that wraps its control names it: no aria-label. */
1909
+ ["div", {class: "SCHEMA_TOPIC_FORM_MAIN field mb-3"}, [
1910
+ ["label", {class: "checkbox"}, [
1911
+ ["input", el_attrs({type: "checkbox", "data-name": "main_topic",
1912
+ checked: record.main_topic ? "checked" : undefined})],
1913
+ ["span", {class: "ml-1", i18n: "main topic"}, t("main topic")]
1914
+ ]]
1915
+ ]],
1901
1916
  ["div", {class: "SCHEMA_TOPIC_FORM_ACTIONS is-flex mt-4",
1902
1917
  style: "gap:.5rem; justify-content:flex-end;"}, [
1903
1918
  ["button", {class: "SCHEMA_TOPIC_FORM_CANCEL button", type: "button"},
@@ -2501,6 +2516,7 @@ function topic_record(gobj, treedb, topic, values)
2501
2516
  record.tkey = values.tkey || "";
2502
2517
  record.topic_version = values.topic_version === "" ? 1 : Number(values.topic_version);
2503
2518
  record.system_topic = !!values.system_topic;
2519
+ record.main_topic = !!values.main_topic;
2504
2520
 
2505
2521
  return record;
2506
2522
  }