@yuneta/gobj-ui 2.6.0 → 4.0.0

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.
Files changed (56) hide show
  1. package/README.md +331 -11
  2. package/dist/gobj-ui.cjs.js +21386 -17919
  3. package/dist/gobj-ui.es.js +26059 -22613
  4. package/index.js +45 -9
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.css +9 -0
  8. package/src/c_yui_form.js +113 -53
  9. package/src/c_yui_gobj_tree_js.js +58 -36
  10. package/src/c_yui_json.css +139 -0
  11. package/src/c_yui_json.js +928 -0
  12. package/src/c_yui_json_graph.js +110 -20
  13. package/src/c_yui_map.js +18 -36
  14. package/src/c_yui_nav.js +6 -9
  15. package/src/c_yui_period.css +184 -0
  16. package/src/c_yui_period.js +1441 -0
  17. package/src/c_yui_shell.css +120 -0
  18. package/src/c_yui_shell.js +672 -111
  19. package/src/c_yui_treedb_graph.js +639 -37
  20. package/src/c_yui_treedb_schema.js +478 -0
  21. package/src/c_yui_treedb_topic_with_form.css +18 -0
  22. package/src/c_yui_treedb_topic_with_form.js +153 -103
  23. package/src/c_yui_treedb_topics.css +73 -0
  24. package/src/c_yui_treedb_topics.js +1070 -29
  25. package/src/c_yui_window.css +22 -0
  26. package/src/c_yui_window.js +182 -97
  27. package/src/c_yui_window_manager.js +38 -4
  28. package/src/json_view_helpers.js +214 -0
  29. package/src/json_view_helpers.test.js +135 -0
  30. package/src/route_map_model.js +317 -0
  31. package/src/route_map_model.test.js +209 -0
  32. package/src/route_resolver.js +24 -1
  33. package/src/route_resolver.test.js +40 -1
  34. package/src/shell_modals.js +162 -52
  35. package/src/shell_route_map.css +225 -0
  36. package/src/shell_route_map.js +363 -0
  37. package/src/tabulator.css +245 -0
  38. package/src/yui_dev.js +130 -8
  39. package/src/yui_frontend_view.js +106 -0
  40. package/src/yui_icons.css +62 -0
  41. package/src/yui_inputs.css +8 -3
  42. package/src/yui_inputs.js +51 -8
  43. package/src/yui_tabulator_i18n.js +128 -0
  44. package/src/yui_theme.js +147 -0
  45. package/src/yui_time.js +666 -0
  46. package/src/yui_time.test.js +330 -0
  47. package/src/yui_toolbar.css +28 -0
  48. package/src/yui_toolbar.js +86 -43
  49. package/src/c_yui_main.css +0 -501
  50. package/src/c_yui_main.js +0 -1623
  51. package/src/c_yui_routing.css +0 -18
  52. package/src/c_yui_routing.js +0 -837
  53. package/src/c_yui_tabs.js +0 -487
  54. package/src/themes.js +0 -215
  55. package/src/ytable.css +0 -63
  56. package/src/ytable.js +0 -505
@@ -0,0 +1,928 @@
1
+ /***********************************************************************
2
+ * c_yui_json.js
3
+ *
4
+ * Lazy JSON tree viewer.
5
+ *
6
+ * Container-agnostic (like C_YUI_PAGER): the gclass owns ONLY the
7
+ * viewer chrome (a toolbar + a scrollable tree body). The parent
8
+ * mounts `gobj_read_attr(json_view, "$container")` wherever it wants
9
+ * (a C_YUI_WINDOW body, a Bulma modal-card, or inline) and feeds it
10
+ * JSON with EV_SET_JSON.
11
+ *
12
+ * Built for ARBITRARILY LARGE JSON via server-driven lazy expansion.
13
+ * The kernel's `kw_collapse()` (kwid.c) truncates over-limit dicts and
14
+ * arrays into sentinels:
15
+ * { "__collapsed__": { "path": ..., "size": N } }
16
+ * The viewer renders those as an expandable stub; opening one does NOT
17
+ * fetch anything itself — it PUBLISHES EV_EXPAND_PATH {path, size} to
18
+ * its subscriber, which is the ONLY party that knows the backend (e.g.
19
+ * it re-issues `print-tranger path=<path>`), then hands the subtree
20
+ * back with EV_SUBTREE_LOADED {path, json}. This keeps the component
21
+ * decoupled from any command / transport. With no sentinels present
22
+ * it degrades to a plain client-side collapsible tree.
23
+ *
24
+ * Only expanded containers are materialised in the DOM, so the tree
25
+ * stays bounded no matter how large the source document is.
26
+ *
27
+ * DOM is self-describing (UPPER_SNAKE logical classes): JSON_VIEWER /
28
+ * JSON_TOOLBAR / JSON_SEARCH / JSON_TREE / JSON_ROW / JSON_KEY /
29
+ * JSON_VALUE / JSON_SUMMARY / JSON_COLLAPSED / JSON_TIME.
30
+ *
31
+ * Copyright (c) 2026, ArtGins.
32
+ * All Rights Reserved.
33
+ ***********************************************************************/
34
+ import {
35
+ SDATA,
36
+ SDATA_END,
37
+ data_type_t,
38
+ event_flag_t,
39
+ gclass_create,
40
+ log_error,
41
+ gobj_read_pointer_attr,
42
+ gobj_parent,
43
+ gobj_subscribe_event,
44
+ gobj_read_attr,
45
+ gobj_write_attr,
46
+ gobj_read_str_attr,
47
+ gobj_send_event,
48
+ gobj_publish_event,
49
+ createElement2,
50
+ json_deep_copy,
51
+ json_object_size,
52
+ refresh_language,
53
+ } from "@yuneta/gobj-js";
54
+
55
+ import {
56
+ json_type,
57
+ is_collapsed,
58
+ seg_join,
59
+ seg_split,
60
+ set_by_segments,
61
+ subtree_matches,
62
+ is_time_field,
63
+ format_epoch,
64
+ } from "./json_view_helpers.js";
65
+
66
+ import {yui_toolbar} from "./yui_toolbar.js";
67
+ import {attach_clear} from "./yui_inputs.js";
68
+
69
+ import {t} from "i18next";
70
+
71
+ import "./c_yui_json.css";
72
+
73
+
74
+ /***************************************************************
75
+ * Constants
76
+ ***************************************************************/
77
+ const GCLASS_NAME = "C_YUI_JSON";
78
+
79
+ /*
80
+ * Hard cap on rows painted in one render pass. A guard against an
81
+ * accidental "expand all" over a giant already-loaded document; the
82
+ * cap is announced in the tree (never silently truncated).
83
+ */
84
+ const MAX_RENDER_ROWS = 5000;
85
+
86
+ /***************************************************************
87
+ * Data
88
+ ***************************************************************/
89
+ const attrs_table = [
90
+ /*---------------- Public Attributes ----------------*/
91
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
92
+
93
+ /*---------------- Config ----------------*/
94
+ SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
95
+ SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
96
+
97
+ /*---------------- UI ----------------*/
98
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
99
+ SDATA_END()
100
+ ];
101
+
102
+ let PRIVATE_DATA = {
103
+ root: null, // working JSON tree (deep-copied, mutated on splice)
104
+ expanded: null, // Set<string> of expanded absolute paths
105
+ pending: null, // Set<string> of paths whose subtree is being fetched
106
+ errors: null, // Map<string,string> of per-path expand errors
107
+ search: "", // current search term (lower-cased)
108
+ $tree: null, // the scrollable tree body element
109
+ $search: null, // the search input element
110
+ };
111
+
112
+ let __gclass__ = null;
113
+
114
+
115
+
116
+
117
+ /******************************
118
+ * Framework Methods
119
+ ******************************/
120
+
121
+
122
+
123
+
124
+ /***************************************************************
125
+ * Framework Method: Create
126
+ ***************************************************************/
127
+ function mt_create(gobj)
128
+ {
129
+ let priv = gobj.priv;
130
+
131
+ priv.expanded = new Set();
132
+ priv.pending = new Set();
133
+ priv.errors = new Map();
134
+ priv.search = "";
135
+
136
+ let json_data = gobj_read_attr(gobj, "json_data");
137
+ if(json_data !== null && json_data !== undefined) {
138
+ priv.root = json_deep_copy(json_data);
139
+ }
140
+
141
+ build_ui(gobj);
142
+
143
+ /*
144
+ * CHILD subscription model
145
+ */
146
+ let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
147
+ if(!subscriber) {
148
+ subscriber = gobj_parent(gobj);
149
+ }
150
+ gobj_subscribe_event(gobj, null, {}, subscriber);
151
+ }
152
+
153
+ /***************************************************************
154
+ * Framework Method: Start
155
+ ***************************************************************/
156
+ function mt_start(gobj)
157
+ {
158
+ render_tree(gobj);
159
+ }
160
+
161
+ /***************************************************************
162
+ * Framework Method: Stop
163
+ ***************************************************************/
164
+ function mt_stop(gobj)
165
+ {
166
+ }
167
+
168
+ /***************************************************************
169
+ * Framework Method: Destroy
170
+ ***************************************************************/
171
+ function mt_destroy(gobj)
172
+ {
173
+ destroy_ui(gobj);
174
+ }
175
+
176
+
177
+
178
+
179
+ /***************************
180
+ * Local Methods
181
+ ***************************/
182
+
183
+
184
+
185
+
186
+ /************************************************************
187
+ * Build UI
188
+ ************************************************************/
189
+ function build_ui(gobj)
190
+ {
191
+ let priv = gobj.priv;
192
+
193
+ let $toolbar = make_toolbar(gobj);
194
+
195
+ let $container = createElement2(
196
+ ['div', {class: 'C_YUI_JSON JSON_VIEWER view-card',
197
+ style: 'height:100%; display:flex; flex-direction:column;'}, [
198
+ ['div', {class: 'JSON_TOOLBAR is-flex-grow-0'}, [$toolbar]],
199
+ ['div', {class: 'JSON_TREE is-flex-grow-1',
200
+ style: 'flex:1 1 auto; min-height:0; overflow:auto;'}, []]
201
+ ]]
202
+ );
203
+
204
+ gobj_write_attr(gobj, "$container", $container);
205
+ priv.$tree = $container.querySelector('.JSON_TREE');
206
+ priv.$search = $container.querySelector('.JSON_SEARCH');
207
+
208
+ refresh_language($container, t);
209
+ }
210
+
211
+ /************************************************************
212
+ * Destroy UI
213
+ ************************************************************/
214
+ function destroy_ui(gobj)
215
+ {
216
+ let priv = gobj.priv;
217
+ let $container = gobj_read_attr(gobj, "$container");
218
+ if($container) {
219
+ if($container.parentNode) {
220
+ $container.parentNode.removeChild($container);
221
+ }
222
+ gobj_write_attr(gobj, "$container", null);
223
+ }
224
+ priv.$tree = null;
225
+ priv.$search = null;
226
+ }
227
+
228
+ /************************************************************
229
+ * Toolbar: search + expand-all / collapse-all / copy
230
+ ************************************************************/
231
+ function make_toolbar(gobj)
232
+ {
233
+ let title = gobj_read_str_attr(gobj, "title");
234
+
235
+ let left_items = [];
236
+ if(title) {
237
+ left_items.push(
238
+ ['span', {class: 'JSON_TITLE is-flex is-align-items-center px-2',
239
+ style: 'font-weight:600;', 'data-i18n': title}, title]
240
+ );
241
+ }
242
+ /* Search box: a magnifier on the left and the NORM clear (✕) via
243
+ * attach_clear on the right. Clearing dispatches a synthetic `input`,
244
+ * which re-fires EV_SEARCH with an empty term through the FSM. */
245
+ let $search_input = createElement2(
246
+ ['input', {class: 'JSON_SEARCH input', type: 'text',
247
+ placeholder: t("search")}, [], {
248
+ input: function(evt) {
249
+ gobj_send_event(gobj, "EV_SEARCH", {text: evt.target.value}, gobj);
250
+ }
251
+ }]);
252
+ let $search_control = createElement2(
253
+ ['div', {class: 'control has-icons-left', style: 'max-width:22em;'}, [
254
+ $search_input,
255
+ ['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
256
+ ]]);
257
+ attach_clear($search_control, $search_input);
258
+ left_items.push($search_control);
259
+
260
+ let right_items = [
261
+ icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
262
+ icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
263
+ icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
264
+ ];
265
+
266
+ const $toolbar = yui_toolbar({}, [
267
+ ['div', {class: 'yui-horizontal-toolbar-section left is-flex is-align-items-center',
268
+ style: 'gap:.25rem;'}, left_items],
269
+ ['div', {class: 'yui-horizontal-toolbar-section center'}, []],
270
+ ['div', {class: 'yui-horizontal-toolbar-section right is-flex is-align-items-center',
271
+ style: 'gap:.25rem;'}, right_items],
272
+ ]);
273
+
274
+ refresh_language($toolbar, t);
275
+ return $toolbar;
276
+ }
277
+
278
+ /************************************************************
279
+ * A single icon toolbar button that fires `event_name`
280
+ ************************************************************/
281
+ function icon_button(gobj, icon, event_name, label_key)
282
+ {
283
+ return ['button', {class: `button ${event_name}`, style: 'width:2.5em;',
284
+ title: t(label_key), 'data-i18n-title': label_key,
285
+ 'aria-label': t(label_key), 'data-i18n-aria-label': label_key}, [
286
+ ['span', {class: 'icon'}, [['i', {class: icon}]]]
287
+ ], {
288
+ click: function(evt) {
289
+ evt.stopPropagation();
290
+ gobj_send_event(gobj, event_name, {}, gobj);
291
+ }
292
+ }];
293
+ }
294
+
295
+ /************************************************************
296
+ * Re-render the whole tree from priv.root + priv.expanded.
297
+ *
298
+ * Only expanded containers are walked, so the DOM size is
299
+ * bounded by what the user opened, not by the document size.
300
+ ************************************************************/
301
+ function render_tree(gobj)
302
+ {
303
+ let priv = gobj.priv;
304
+ let $tree = priv.$tree;
305
+ if(!$tree) {
306
+ return;
307
+ }
308
+
309
+ let scroll_top = $tree.scrollTop;
310
+ $tree.textContent = "";
311
+
312
+ if(priv.root === null || priv.root === undefined) {
313
+ $tree.appendChild(createElement2(
314
+ ['div', {class: 'JSON_EMPTY has-text-grey p-3', 'data-i18n': 'no data'}, 'no data']
315
+ ));
316
+ return;
317
+ }
318
+
319
+ let ctx = {gobj: gobj, term: priv.search, count: 0, capped: false};
320
+ let rows = [];
321
+
322
+ let type = json_type(priv.root);
323
+ if(is_collapsed(priv.root)) {
324
+ push_collapsed_row(ctx, priv.root, [], 0, "", rows);
325
+ } else if(type === "object") {
326
+ for(let [k, v] of Object.entries(priv.root)) {
327
+ push_entry_rows(ctx, k, v, [k], 0, rows);
328
+ }
329
+ } else if(type === "array") {
330
+ priv.root.forEach(function(v, i) {
331
+ push_entry_rows(ctx, i, v, [String(i)], 0, rows);
332
+ });
333
+ } else {
334
+ push_entry_rows(ctx, null, priv.root, [], 0, rows);
335
+ }
336
+
337
+ if(ctx.capped) {
338
+ rows.push(['div', {class: 'JSON_CAPPED has-text-warning p-2',
339
+ 'data-i18n': 'too many rows; collapse some branches'},
340
+ 'too many rows; collapse some branches']);
341
+ }
342
+
343
+ $tree.appendChild(createElement2(['div', {class: 'JSON_ROWS'}, rows]));
344
+ refresh_language($tree, t);
345
+ $tree.scrollTop = scroll_top;
346
+ }
347
+
348
+ /************************************************************
349
+ * Render one entry (key -> value) into `rows`.
350
+ * Dispatches to collapsed-stub / container / leaf.
351
+ ************************************************************/
352
+ function push_entry_rows(ctx, key, value, segments, depth, rows)
353
+ {
354
+ if(ctx.count >= MAX_RENDER_ROWS) {
355
+ ctx.capped = true;
356
+ return;
357
+ }
358
+
359
+ let term = ctx.term;
360
+ let key_match = term && key !== null && String(key).toLowerCase().includes(term);
361
+
362
+ if(is_collapsed(value)) {
363
+ /*
364
+ * A not-yet-loaded subtree: searchable only by its key.
365
+ */
366
+ if(term && !key_match) {
367
+ return;
368
+ }
369
+ push_collapsed_row(ctx, value, segments, depth, key, rows);
370
+ return;
371
+ }
372
+
373
+ let type = json_type(value);
374
+ if(type === "object" || type === "array") {
375
+ if(term && !key_match && !subtree_matches(value, term)) {
376
+ return;
377
+ }
378
+ push_container_rows(ctx, key, value, segments, depth, rows, key_match);
379
+ return;
380
+ }
381
+
382
+ /*
383
+ * Leaf (primitive)
384
+ */
385
+ if(term && !key_match && !String_of(value).toLowerCase().includes(term)) {
386
+ return;
387
+ }
388
+ ctx.count++;
389
+ rows.push(leaf_row(key, value, depth));
390
+ }
391
+
392
+ /************************************************************
393
+ * Container (object/array) row + its expanded children
394
+ ************************************************************/
395
+ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
396
+ {
397
+ let priv = ctx.gobj.priv;
398
+ let path = seg_join(segments);
399
+ let is_object = json_type(value) === "object";
400
+ let size = is_object ? json_object_size(value) : value.length;
401
+
402
+ /*
403
+ * Auto-expand while searching so matches are visible; otherwise
404
+ * honour the user's expand/collapse state.
405
+ */
406
+ let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
407
+ let open = priv.expanded.has(path) || searching_match;
408
+
409
+ ctx.count++;
410
+ rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
411
+
412
+ if(!open) {
413
+ return;
414
+ }
415
+
416
+ if(is_object) {
417
+ for(let [k, v] of Object.entries(value)) {
418
+ push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
419
+ }
420
+ } else {
421
+ value.forEach(function(v, i) {
422
+ push_entry_rows(ctx, i, v, segments.concat(String(i)), depth + 1, rows);
423
+ });
424
+ }
425
+ }
426
+
427
+ /************************************************************
428
+ * Collapsed-sentinel stub row (fetch-on-open)
429
+ ************************************************************/
430
+ function push_collapsed_row(ctx, value, segments, depth, key, rows)
431
+ {
432
+ let priv = ctx.gobj.priv;
433
+ let path = seg_join(segments);
434
+ let info = is_collapsed(value) || {};
435
+ let size = info.size;
436
+ let is_pending = priv.pending.has(path);
437
+ let err = priv.errors.get(path);
438
+
439
+ ctx.count++;
440
+
441
+ let key_spec = (key === null || key === "")
442
+ ? null
443
+ : ['span', {class: 'JSON_KEY'}, String(key)];
444
+
445
+ let stub_text = (info.is_array ? "[" : "{") +
446
+ (size !== undefined ? String(size) : "?") +
447
+ (info.is_array ? "]" : "}");
448
+
449
+ let children = [];
450
+ if(key_spec) {
451
+ children.push(key_spec);
452
+ children.push(['span', {class: 'JSON_PUNCT'}, ': ']);
453
+ }
454
+ children.push(['span', {class: 'JSON_STUB'}, stub_text]);
455
+ children.push(['span', {class: 'JSON_STUB_HINT is-size-7 ml-2',
456
+ 'data-i18n': is_pending ? 'loading' : 'click to load'},
457
+ is_pending ? 'loading' : 'click to load']);
458
+ if(err) {
459
+ children.push(['span', {class: 'JSON_STUB_ERR has-text-danger is-size-7 ml-2'}, String(err)]);
460
+ }
461
+
462
+ let attrs = {
463
+ class: 'JSON_ROW JSON_COLLAPSED' + (is_pending ? ' is-pending' : ''),
464
+ style: row_indent(depth),
465
+ };
466
+ let events = is_pending ? undefined : {
467
+ click: function(evt) {
468
+ evt.stopPropagation();
469
+ gobj_send_event(ctx.gobj, "EV_EXPAND_COLLAPSED",
470
+ {path: path, size: size}, ctx.gobj);
471
+ }
472
+ };
473
+
474
+ let content = [
475
+ ['span', {class: 'JSON_TOGGLE JSON_TOGGLE_REMOTE'}, [['i', {class: 'yi-plus'}]]],
476
+ ['span', {class: 'JSON_ROW_BODY'}, children],
477
+ ];
478
+ rows.push(events ? ['div', attrs, content, events] : ['div', attrs, content]);
479
+ }
480
+
481
+ /************************************************************
482
+ * Expandable container header row
483
+ ************************************************************/
484
+ function toggle_row(gobj, key, size, is_object, depth, path, open)
485
+ {
486
+ let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
487
+
488
+ let body = [];
489
+ if(key !== null && key !== "") {
490
+ body.push(['span', {class: 'JSON_KEY'}, String(key)]);
491
+ body.push(['span', {class: 'JSON_PUNCT'}, ': ']);
492
+ }
493
+ body.push(['span', {class: 'JSON_SUMMARY has-text-grey'}, summary]);
494
+
495
+ return ['div', {class: 'JSON_ROW JSON_CONTAINER', style: row_indent(depth)}, [
496
+ ['span', {class: 'JSON_TOGGLE' + (open ? ' is-open' : '')}, [
497
+ ['i', {class: 'yi-chevron-right'}]
498
+ ]],
499
+ ['span', {class: 'JSON_ROW_BODY'}, body],
500
+ ], {
501
+ click: function(evt) {
502
+ evt.stopPropagation();
503
+ gobj_send_event(gobj, "EV_TOGGLE_NODE", {path: path}, gobj);
504
+ }
505
+ }];
506
+ }
507
+
508
+ /************************************************************
509
+ * Leaf (primitive) row, type-coloured, timestamp-tagged
510
+ ************************************************************/
511
+ function leaf_row(key, value, depth)
512
+ {
513
+ let type = json_type(value);
514
+ let text;
515
+ switch(type) {
516
+ case "string":
517
+ text = '"' + value + '"';
518
+ break;
519
+ case "null":
520
+ text = "null";
521
+ break;
522
+ case "boolean":
523
+ text = value ? "true" : "false";
524
+ break;
525
+ default:
526
+ text = String(value);
527
+ break;
528
+ }
529
+
530
+ let body = [];
531
+ if(key !== null && key !== "") {
532
+ body.push(['span', {class: 'JSON_KEY'}, String(key)]);
533
+ body.push(['span', {class: 'JSON_PUNCT'}, ': ']);
534
+ }
535
+ body.push(['span', {class: 'JSON_VALUE JSON_TYPE_' + type.toUpperCase()}, text]);
536
+
537
+ if(key !== null && is_time_field(String(key))) {
538
+ let wall = format_epoch(value);
539
+ if(wall) {
540
+ body.push(['span', {class: 'JSON_TIME is-size-7 has-text-grey ml-2'}, wall]);
541
+ }
542
+ }
543
+
544
+ return ['div', {class: 'JSON_ROW JSON_LEAF', style: row_indent(depth)}, [
545
+ ['span', {class: 'JSON_TOGGLE JSON_TOGGLE_SPACER'}, []],
546
+ ['span', {class: 'JSON_ROW_BODY'}, body],
547
+ ]];
548
+ }
549
+
550
+ /************************************************************
551
+ * Depth indentation (inline style keeps it CSS-framework free)
552
+ ************************************************************/
553
+ function row_indent(depth)
554
+ {
555
+ return 'padding-left:' + (0.4 + depth * 1.1) + 'em;';
556
+ }
557
+
558
+ /************************************************************
559
+ * String() that never throws on a non-primitive
560
+ ************************************************************/
561
+ function String_of(value)
562
+ {
563
+ try {
564
+ return String(value);
565
+ } catch(e) {
566
+ return "";
567
+ }
568
+ }
569
+
570
+ /************************************************************
571
+ * Recursively collect every loaded container path (for
572
+ * "expand loaded"). Collapsed sentinels are NOT expanded.
573
+ ************************************************************/
574
+ function collect_loaded_paths(value, segments, out)
575
+ {
576
+ if(is_collapsed(value)) {
577
+ return;
578
+ }
579
+ let type = json_type(value);
580
+ if(type === "object") {
581
+ out.push(seg_join(segments));
582
+ for(let [k, v] of Object.entries(value)) {
583
+ collect_loaded_paths(v, segments.concat(k), out);
584
+ }
585
+ } else if(type === "array") {
586
+ out.push(seg_join(segments));
587
+ value.forEach(function(v, i) {
588
+ collect_loaded_paths(v, segments.concat(String(i)), out);
589
+ });
590
+ }
591
+ }
592
+
593
+
594
+
595
+
596
+ /***************************
597
+ * Actions
598
+ ***************************/
599
+
600
+
601
+
602
+
603
+ /************************************************************
604
+ * EV_SET_JSON { json } — replace the whole document
605
+ ************************************************************/
606
+ function ac_set_json(gobj, event, kw, src)
607
+ {
608
+ let priv = gobj.priv;
609
+
610
+ priv.root = (kw.json === undefined || kw.json === null)
611
+ ? null
612
+ : json_deep_copy(kw.json);
613
+ priv.expanded.clear();
614
+ priv.pending.clear();
615
+ priv.errors.clear();
616
+
617
+ render_tree(gobj);
618
+ return 0;
619
+ }
620
+
621
+ /************************************************************
622
+ * EV_SUBTREE_LOADED { path, json } — splice a fetched subtree
623
+ ************************************************************/
624
+ function ac_subtree_loaded(gobj, event, kw, src)
625
+ {
626
+ let priv = gobj.priv;
627
+
628
+ let path = kw.path || "";
629
+ let segments = seg_split(path);
630
+
631
+ if(segments.length === 0) {
632
+ priv.root = (kw.json === undefined) ? null : json_deep_copy(kw.json);
633
+ } else {
634
+ priv.root = set_by_segments(priv.root, segments, json_deep_copy(kw.json));
635
+ }
636
+
637
+ priv.pending.delete(path);
638
+ priv.errors.delete(path);
639
+ if(path) {
640
+ priv.expanded.add(path); // reveal what we just loaded
641
+ }
642
+
643
+ render_tree(gobj);
644
+ return 0;
645
+ }
646
+
647
+ /************************************************************
648
+ * EV_SUBTREE_ERROR { path, error } — mark the failed branch
649
+ ************************************************************/
650
+ function ac_subtree_error(gobj, event, kw, src)
651
+ {
652
+ let priv = gobj.priv;
653
+
654
+ let path = kw.path || "";
655
+ priv.pending.delete(path);
656
+ priv.errors.set(path, kw.error || "error");
657
+
658
+ log_error(`${GCLASS_NAME}: subtree load failed at '${path}': ${kw.error || ""}`);
659
+
660
+ render_tree(gobj);
661
+ return 0;
662
+ }
663
+
664
+ /************************************************************
665
+ * EV_TOGGLE_NODE { path } — expand/collapse a loaded container
666
+ ************************************************************/
667
+ function ac_toggle_node(gobj, event, kw, src)
668
+ {
669
+ let priv = gobj.priv;
670
+ let path = kw.path || "";
671
+
672
+ if(priv.expanded.has(path)) {
673
+ priv.expanded.delete(path);
674
+ } else {
675
+ priv.expanded.add(path);
676
+ }
677
+
678
+ render_tree(gobj);
679
+ return 0;
680
+ }
681
+
682
+ /************************************************************
683
+ * EV_EXPAND_COLLAPSED { path, size } — ask the subscriber to
684
+ * load a truncated subtree, then republish EV_EXPAND_PATH.
685
+ ************************************************************/
686
+ function ac_expand_collapsed(gobj, event, kw, src)
687
+ {
688
+ let priv = gobj.priv;
689
+ let path = kw.path || "";
690
+
691
+ if(priv.pending.has(path)) {
692
+ return 0;
693
+ }
694
+ priv.pending.add(path);
695
+ priv.errors.delete(path);
696
+
697
+ render_tree(gobj); // show the "loading…" state
698
+
699
+ gobj_publish_event(gobj, "EV_EXPAND_PATH", {path: path, size: kw.size});
700
+ return 0;
701
+ }
702
+
703
+ /************************************************************
704
+ * EV_SEARCH { text }
705
+ ************************************************************/
706
+ function ac_search(gobj, event, kw, src)
707
+ {
708
+ let priv = gobj.priv;
709
+ priv.search = (kw.text || "").trim().toLowerCase();
710
+ render_tree(gobj);
711
+ return 0;
712
+ }
713
+
714
+ /************************************************************
715
+ * EV_EXPAND_ALL — expand every already-loaded container
716
+ ************************************************************/
717
+ function ac_expand_all(gobj, event, kw, src)
718
+ {
719
+ let priv = gobj.priv;
720
+ if(priv.root === null || priv.root === undefined) {
721
+ return 0;
722
+ }
723
+
724
+ let paths = [];
725
+ let type = json_type(priv.root);
726
+ if(type === "object") {
727
+ for(let [k, v] of Object.entries(priv.root)) {
728
+ collect_loaded_paths(v, [k], paths);
729
+ }
730
+ } else if(type === "array") {
731
+ priv.root.forEach(function(v, i) {
732
+ collect_loaded_paths(v, [String(i)], paths);
733
+ });
734
+ }
735
+ paths.forEach(function(p) {
736
+ priv.expanded.add(p);
737
+ });
738
+
739
+ render_tree(gobj);
740
+ return 0;
741
+ }
742
+
743
+ /************************************************************
744
+ * EV_COLLAPSE_ALL
745
+ ************************************************************/
746
+ function ac_collapse_all(gobj, event, kw, src)
747
+ {
748
+ let priv = gobj.priv;
749
+ priv.expanded.clear();
750
+ render_tree(gobj);
751
+ return 0;
752
+ }
753
+
754
+ /************************************************************
755
+ * EV_COPY_ALL — copy the working document to the clipboard
756
+ ************************************************************/
757
+ function ac_copy_all(gobj, event, kw, src)
758
+ {
759
+ let priv = gobj.priv;
760
+ if(priv.root === null || priv.root === undefined) {
761
+ return 0;
762
+ }
763
+ let text = JSON.stringify(priv.root, null, 2);
764
+ if(navigator.clipboard && navigator.clipboard.writeText) {
765
+ navigator.clipboard.writeText(text).catch(function(e) {
766
+ log_error(`${GCLASS_NAME}: clipboard write failed: ${e}`);
767
+ });
768
+ } else {
769
+ log_error(`${GCLASS_NAME}: clipboard API unavailable`);
770
+ }
771
+ return 0;
772
+ }
773
+
774
+ /************************************************************
775
+ * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
776
+ ************************************************************/
777
+ function ac_language_changed(gobj, event, kw, src)
778
+ {
779
+ let priv = gobj.priv;
780
+ let $container = gobj_read_attr(gobj, "$container");
781
+ if($container) {
782
+ refresh_language($container, t);
783
+ if(priv.$search) {
784
+ priv.$search.setAttribute("placeholder", t("search"));
785
+ }
786
+ }
787
+ render_tree(gobj);
788
+ return 0;
789
+ }
790
+
791
+ /************************************************************
792
+ * EV_SHOW / EV_HIDE — host visibility
793
+ ************************************************************/
794
+ function ac_show(gobj, event, kw, src)
795
+ {
796
+ let $container = gobj_read_attr(gobj, "$container");
797
+ if($container) {
798
+ $container.classList.remove('is-hidden');
799
+ }
800
+ return 0;
801
+ }
802
+
803
+ function ac_hide(gobj, event, kw, src)
804
+ {
805
+ let $container = gobj_read_attr(gobj, "$container");
806
+ if($container) {
807
+ $container.classList.add('is-hidden');
808
+ }
809
+ return 0;
810
+ }
811
+
812
+ /************************************************************
813
+ * EV_REFRESH
814
+ ************************************************************/
815
+ function ac_refresh(gobj, event, kw, src)
816
+ {
817
+ render_tree(gobj);
818
+ return 0;
819
+ }
820
+
821
+
822
+
823
+
824
+ /***************************
825
+ * FSM
826
+ ***************************/
827
+
828
+
829
+
830
+
831
+ /*---------------------------------------------*
832
+ * Global methods table
833
+ *---------------------------------------------*/
834
+ const gmt = {
835
+ mt_create: mt_create,
836
+ mt_start: mt_start,
837
+ mt_stop: mt_stop,
838
+ mt_destroy: mt_destroy,
839
+ };
840
+
841
+ /***************************************************************
842
+ * Create the GClass
843
+ ***************************************************************/
844
+ function create_gclass(gclass_name)
845
+ {
846
+ if(__gclass__) {
847
+ log_error(`GClass ALREADY created: ${gclass_name}`);
848
+ return -1;
849
+ }
850
+
851
+ /*---------------------------------------------*
852
+ * States
853
+ *---------------------------------------------*/
854
+ const states = [
855
+ ["ST_EMPTY", [
856
+ ["EV_SET_JSON", ac_set_json, "ST_READY"],
857
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
858
+ ["EV_REFRESH", ac_refresh, null],
859
+ ["EV_SHOW", ac_show, null],
860
+ ["EV_HIDE", ac_hide, null]
861
+ ]],
862
+ ["ST_READY", [
863
+ ["EV_SET_JSON", ac_set_json, null],
864
+ ["EV_SUBTREE_LOADED", ac_subtree_loaded, null],
865
+ ["EV_SUBTREE_ERROR", ac_subtree_error, null],
866
+ ["EV_TOGGLE_NODE", ac_toggle_node, null],
867
+ ["EV_EXPAND_COLLAPSED", ac_expand_collapsed, null],
868
+ ["EV_SEARCH", ac_search, null],
869
+ ["EV_EXPAND_ALL", ac_expand_all, null],
870
+ ["EV_COLLAPSE_ALL", ac_collapse_all, null],
871
+ ["EV_COPY_ALL", ac_copy_all, null],
872
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
873
+ ["EV_REFRESH", ac_refresh, null],
874
+ ["EV_SHOW", ac_show, null],
875
+ ["EV_HIDE", ac_hide, null]
876
+ ]]
877
+ ];
878
+
879
+ /*---------------------------------------------*
880
+ * Events
881
+ *---------------------------------------------*/
882
+ const event_types = [
883
+ ["EV_SET_JSON", 0],
884
+ ["EV_SUBTREE_LOADED", 0],
885
+ ["EV_SUBTREE_ERROR", 0],
886
+ ["EV_TOGGLE_NODE", 0],
887
+ ["EV_EXPAND_COLLAPSED", 0],
888
+ ["EV_SEARCH", 0],
889
+ ["EV_EXPAND_ALL", 0],
890
+ ["EV_COLLAPSE_ALL", 0],
891
+ ["EV_COPY_ALL", 0],
892
+ ["EV_LANGUAGE_CHANGED", 0],
893
+ ["EV_REFRESH", 0],
894
+ ["EV_SHOW", 0],
895
+ ["EV_HIDE", 0],
896
+ ["EV_EXPAND_PATH", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS]
897
+ ];
898
+
899
+ __gclass__ = gclass_create(
900
+ gclass_name,
901
+ event_types,
902
+ states,
903
+ gmt,
904
+ 0, // lmt,
905
+ attrs_table,
906
+ PRIVATE_DATA,
907
+ 0, // authz_table,
908
+ 0, // command_table,
909
+ 0, // s_user_trace_level
910
+ 0 // gclass_flag
911
+ );
912
+
913
+ if(!__gclass__) {
914
+ return -1;
915
+ }
916
+
917
+ return 0;
918
+ }
919
+
920
+ /***************************************************************
921
+ * Register GClass
922
+ ***************************************************************/
923
+ function register_c_yui_json()
924
+ {
925
+ return create_gclass(GCLASS_NAME);
926
+ }
927
+
928
+ export { register_c_yui_json };