@yuneta/gobj-ui 1.0.1 → 2.2.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 (46) hide show
  1. package/README.md +40 -363
  2. package/dist/gobj-ui.cjs.js +11154 -5506
  3. package/dist/gobj-ui.es.js +11131 -5508
  4. package/index.js +41 -14
  5. package/package.json +11 -9
  6. package/src/c_g6_nodes_tree.js +6 -1
  7. package/src/c_yui_form.js +1 -1
  8. package/src/c_yui_gobj_tree_js.js +1 -1
  9. package/src/c_yui_json_graph.js +1 -1
  10. package/src/c_yui_main.js +3 -3
  11. package/src/c_yui_map.js +6 -1
  12. package/src/c_yui_nav.js +881 -0
  13. package/src/c_yui_pager.js +545 -0
  14. package/src/c_yui_routing.css +1 -1
  15. package/src/c_yui_routing.js +1 -1
  16. package/src/c_yui_shell.css +571 -0
  17. package/src/c_yui_shell.js +2474 -0
  18. package/src/c_yui_tabs.js +1 -1
  19. package/src/c_yui_treedb_graph.js +35 -9
  20. package/src/c_yui_treedb_topic_with_form.js +1 -1
  21. package/src/c_yui_treedb_topics.js +36 -8
  22. package/src/c_yui_uplot.js +1 -1
  23. package/src/c_yui_window.js +242 -21
  24. package/src/c_yui_window_manager.js +602 -0
  25. package/src/c_yui_wizard.js +612 -0
  26. package/src/pager_helpers.js +138 -0
  27. package/src/pager_helpers.test.js +140 -0
  28. package/src/route_resolver.js +53 -0
  29. package/src/route_resolver.test.js +82 -0
  30. package/src/shell_focus_trap.js +123 -0
  31. package/src/shell_focus_trap.test.js +299 -0
  32. package/src/shell_modals.js +445 -0
  33. package/src/shell_show_on.js +91 -0
  34. package/src/shell_show_on.test.js +86 -0
  35. package/src/shell_toolbar_helpers.js +221 -0
  36. package/src/shell_toolbar_helpers.test.js +207 -0
  37. package/src/tabulator.css +53 -0
  38. package/src/wizard_helpers.js +117 -0
  39. package/src/wizard_helpers.test.js +122 -0
  40. package/src/yui_dev.js +1257 -362
  41. package/src/yui_icons.css +5 -0
  42. package/src/yui_inputs.css +32 -0
  43. package/src/yui_inputs.js +71 -0
  44. package/vite-plugin-yuneta-html.js +2 -2
  45. package/skeleton/config.json +0 -20
  46. package/skeleton/index.html +0 -37
@@ -0,0 +1,545 @@
1
+ /***********************************************************************
2
+ * c_yui_pager.js
3
+ *
4
+ * Drill-down navigation stack (Pattern A).
5
+ *
6
+ * Container-agnostic: the gclass owns ONLY the navigation chrome
7
+ * (a "<- title" header + a body that stacks panels). The parent
8
+ * mounts `gobj_read_attr(pager, "$container")` wherever it wants
9
+ * (inside a C_YUI_WINDOW body, a Bulma modal-card, or inline) and
10
+ * feeds content with EV_PUSH_PAGE.
11
+ *
12
+ * No Confirm/Cancel chrome on purpose: changes are auto-saved by
13
+ * the content panel itself (e.g. C_YUI_FORM). "<- back" only
14
+ * navigates; popping past the root emits EV_PAGER_EXIT so the host
15
+ * can close. An optional per-page "discard" affordance emits
16
+ * EV_PAGE_DISCARD; the page decides what discard means.
17
+ *
18
+ * A page `content` may be:
19
+ * - a gobj exposing a "$container" attr (its DOM is reused), or
20
+ * - an HTMLElement, or
21
+ * - a createElement2() spec (array).
22
+ *
23
+ * Copyright (c) 2026, ArtGins.
24
+ * All Rights Reserved.
25
+ ***********************************************************************/
26
+ import {
27
+ SDATA,
28
+ SDATA_END,
29
+ data_type_t,
30
+ event_flag_t,
31
+ gclass_create,
32
+ log_error,
33
+ gobj_subscribe_event,
34
+ gobj_read_pointer_attr,
35
+ gobj_parent,
36
+ createElement2,
37
+ gobj_write_attr,
38
+ gobj_read_attr,
39
+ gobj_send_event,
40
+ gobj_publish_event,
41
+ gobj_stop_children,
42
+ } from "@yuneta/gobj-js";
43
+
44
+ import {
45
+ pager_header_model,
46
+ pager_back_action,
47
+ } from "./pager_helpers.js";
48
+
49
+ import {t} from "i18next";
50
+
51
+
52
+ /***************************************************************
53
+ * Constants
54
+ ***************************************************************/
55
+ const GCLASS_NAME = "C_YUI_PAGER";
56
+
57
+ /***************************************************************
58
+ * Data
59
+ ***************************************************************/
60
+ const attrs_table = [
61
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
62
+
63
+ SDATA(data_type_t.DTP_STRING, "root_title", 0, "", "Title shown on the root page"),
64
+ SDATA(data_type_t.DTP_BOOLEAN, "back_on_root", 0, true, "On the root page the back affordance emits EV_PAGER_EXIT"),
65
+ SDATA(data_type_t.DTP_BOOLEAN, "with_discard", 0, false, "Enable the per-page 'discard' affordance"),
66
+
67
+ /*---------------- UI ----------------*/
68
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
69
+ SDATA_END()
70
+ ];
71
+
72
+ let PRIVATE_DATA = {
73
+ stack: null
74
+ };
75
+
76
+ let __gclass__ = null;
77
+
78
+
79
+
80
+
81
+ /******************************
82
+ * Framework Methods
83
+ ******************************/
84
+
85
+
86
+
87
+
88
+ /***************************************************************
89
+ * Framework Method: Create
90
+ ***************************************************************/
91
+ function mt_create(gobj)
92
+ {
93
+ let priv = gobj.priv;
94
+
95
+ priv.stack = [];
96
+
97
+ build_ui(gobj);
98
+
99
+ /*
100
+ * CHILD subscription model
101
+ */
102
+ let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
103
+ if(!subscriber) {
104
+ subscriber = gobj_parent(gobj);
105
+ }
106
+ gobj_subscribe_event(gobj, null, {}, subscriber);
107
+ }
108
+
109
+ /***************************************************************
110
+ * Framework Method: Start
111
+ ***************************************************************/
112
+ function mt_start(gobj)
113
+ {
114
+ }
115
+
116
+ /***************************************************************
117
+ * Framework Method: Stop
118
+ ***************************************************************/
119
+ function mt_stop(gobj)
120
+ {
121
+ gobj_stop_children(gobj);
122
+ }
123
+
124
+ /***************************************************************
125
+ * Framework Method: Destroy
126
+ ***************************************************************/
127
+ function mt_destroy(gobj)
128
+ {
129
+ destroy_ui(gobj);
130
+ }
131
+
132
+
133
+
134
+
135
+ /***************************
136
+ * Local Methods
137
+ ***************************/
138
+
139
+
140
+
141
+
142
+ /************************************************************
143
+ * Build UI
144
+ ************************************************************/
145
+ function build_ui(gobj)
146
+ {
147
+ let $container = createElement2(
148
+ ['div', {class: 'C_YUI_PAGER', style: 'height:100%; display:flex; flex-direction:column;'}, [
149
+ ['div', {class: 'yui-pager-header is-flex is-align-items-center is-flex-grow-0',
150
+ style: 'gap:.25rem; padding:.25rem .25rem;'}, [
151
+ ['button', {class: 'yui-pager-back button is-white is-hidden', 'aria-label': 'back'}, [
152
+ ['span', {class: 'icon'}, ['i', {class: 'yi-arrow-left'}]]
153
+ ], {
154
+ click: function(evt) {
155
+ evt.stopPropagation();
156
+ gobj_send_event(gobj, "EV_POP_PAGE", {}, gobj);
157
+ }
158
+ }],
159
+ ['span', {class: 'yui-pager-title is-flex-grow-1', style: 'font-weight:600;'}, ''],
160
+ ['button', {class: 'yui-pager-discard button is-white is-hidden', 'aria-label': 'discard'}, [
161
+ ['span', {class: 'icon'}, ['i', {class: 'yi-broom'}]],
162
+ ['span', {class: 'is-hidden-mobile', i18n: 'discard'}, 'discard']
163
+ ], {
164
+ click: function(evt) {
165
+ evt.stopPropagation();
166
+ gobj_send_event(gobj, "EV_DISCARD_PAGE", {}, gobj);
167
+ }
168
+ }]
169
+ ]],
170
+ ['div', {class: 'yui-pager-body is-flex-grow-1',
171
+ style: 'flex:1 1 auto; min-height:0; position:relative; overflow:auto;'}, []
172
+ ]
173
+ ]]
174
+ );
175
+ gobj_write_attr(gobj, "$container", $container);
176
+ }
177
+
178
+ /************************************************************
179
+ * Destroy UI
180
+ ************************************************************/
181
+ function destroy_ui(gobj)
182
+ {
183
+ let $container = gobj_read_attr(gobj, "$container");
184
+ if($container) {
185
+ if($container.parentNode) {
186
+ $container.parentNode.removeChild($container);
187
+ }
188
+ gobj_write_attr(gobj, "$container", null);
189
+ }
190
+ }
191
+
192
+ /************************************************************
193
+ * Resolve a page `content` into { $el, gobj }
194
+ ************************************************************/
195
+ function resolve_panel(content)
196
+ {
197
+ if(content instanceof HTMLElement) {
198
+ return { $el: content, gobj: null };
199
+ }
200
+ if(Array.isArray(content)) {
201
+ return { $el: createElement2(content), gobj: null };
202
+ }
203
+ /*
204
+ * Assume a gobj exposing a "$container" attr
205
+ */
206
+ let $el = gobj_read_attr(content, "$container");
207
+ return { $el: $el, gobj: content };
208
+ }
209
+
210
+ /************************************************************
211
+ * Re-render the header chrome from the current stack
212
+ ************************************************************/
213
+ function render_header(gobj)
214
+ {
215
+ let priv = gobj.priv;
216
+ let proj = priv.stack.map(function(e) {
217
+ return { id: e.id, title: e.title, discardable: e.discardable };
218
+ });
219
+ let model = pager_header_model(proj, {
220
+ root_title: gobj_read_attr(gobj, "root_title"),
221
+ back_on_root: gobj_read_attr(gobj, "back_on_root"),
222
+ with_discard: gobj_read_attr(gobj, "with_discard"),
223
+ });
224
+
225
+ let $container = gobj_read_attr(gobj, "$container");
226
+ let $back = $container.querySelector('.yui-pager-back');
227
+ let $title = $container.querySelector('.yui-pager-title');
228
+ let $discard = $container.querySelector('.yui-pager-discard');
229
+
230
+ $back.classList.toggle('is-hidden', !model.show_back);
231
+ /*
232
+ * Root (back_on_root) shows a close cross INSIDE the popup;
233
+ * deeper pages show a back arrow.
234
+ */
235
+ let $back_icon = $back.querySelector('i');
236
+ if(model.back_kind === "close") {
237
+ $back_icon.className = 'yi-xmark';
238
+ $back.setAttribute('aria-label', 'close');
239
+ } else {
240
+ $back_icon.className = 'yi-arrow-left';
241
+ $back.setAttribute('aria-label', 'back');
242
+ }
243
+ $discard.classList.toggle('is-hidden', !model.show_discard);
244
+ /* render_header runs on every push/pop/replace, AFTER the host's
245
+ * one-time refresh_language($container,t), so the title must be
246
+ * translated HERE. Also stamp the canonical `data-i18n` (NOT a
247
+ * bare `i18n`, which refresh_language ignores) so a later
248
+ * language switch re-translates it. A non-keyed free-form title
249
+ * passes through unchanged (i18next returns the key as-is). */
250
+ $title.setAttribute('data-i18n', model.title);
251
+ $title.textContent = t(model.title);
252
+ }
253
+
254
+ /************************************************************
255
+ * Forward an event to the current top page (if it is a gobj)
256
+ ************************************************************/
257
+ function forward_top(gobj, event, kw)
258
+ {
259
+ let priv = gobj.priv;
260
+ let top = priv.stack[priv.stack.length - 1];
261
+ if(top && top.gobj) {
262
+ gobj_send_event(top.gobj, event, kw || {}, gobj);
263
+ }
264
+ }
265
+
266
+
267
+
268
+
269
+ /***************************
270
+ * Actions
271
+ ***************************/
272
+
273
+
274
+
275
+
276
+ /************************************************************
277
+ * EV_PUSH_PAGE { id, title, content, discardable? }
278
+ ************************************************************/
279
+ function ac_push_page(gobj, event, kw, src)
280
+ {
281
+ let priv = gobj.priv;
282
+
283
+ let resolved = resolve_panel(kw.content);
284
+ if(!resolved.$el) {
285
+ log_error(`${GCLASS_NAME}: EV_PUSH_PAGE without content`);
286
+ return -1;
287
+ }
288
+
289
+ let $body = gobj_read_attr(gobj, "$container").querySelector('.yui-pager-body');
290
+
291
+ let cur = priv.stack[priv.stack.length - 1];
292
+ if(cur && cur.$el) {
293
+ cur.$el.classList.add('is-hidden');
294
+ }
295
+
296
+ resolved.$el.classList.remove('is-hidden');
297
+ if(resolved.$el.parentNode !== $body) {
298
+ $body.appendChild(resolved.$el);
299
+ }
300
+
301
+ priv.stack.push({
302
+ id: kw.id || "",
303
+ title: kw.title || "",
304
+ discardable: !!kw.discardable,
305
+ $el: resolved.$el,
306
+ gobj: resolved.gobj,
307
+ });
308
+
309
+ render_header(gobj);
310
+ gobj_publish_event(gobj, "EV_PAGE_SHOWN", {id: kw.id || "", depth: priv.stack.length});
311
+ return 0;
312
+ }
313
+
314
+ /************************************************************
315
+ * EV_POP_PAGE / EV_BACK
316
+ ************************************************************/
317
+ function ac_pop_page(gobj, event, kw, src)
318
+ {
319
+ let priv = gobj.priv;
320
+
321
+ let proj = priv.stack.map(function(e) {
322
+ return { id: e.id };
323
+ });
324
+ let action = pager_back_action(proj, gobj_read_attr(gobj, "back_on_root"));
325
+
326
+ if(action.type === "noop") {
327
+ return 0;
328
+ }
329
+ if(action.type === "exit") {
330
+ gobj_publish_event(gobj, "EV_PAGER_EXIT", {});
331
+ return 0;
332
+ }
333
+
334
+ /*
335
+ * pop: detach the top panel (do NOT destroy it, the parent owns it)
336
+ */
337
+ let popped = priv.stack.pop();
338
+ if(popped && popped.$el && popped.$el.parentNode) {
339
+ popped.$el.parentNode.removeChild(popped.$el);
340
+ }
341
+
342
+ let top = priv.stack[priv.stack.length - 1];
343
+ if(top && top.$el) {
344
+ top.$el.classList.remove('is-hidden');
345
+ }
346
+
347
+ render_header(gobj);
348
+ gobj_publish_event(gobj, "EV_PAGE_SHOWN", {
349
+ id: top ? top.id : "",
350
+ depth: priv.stack.length,
351
+ });
352
+ return 0;
353
+ }
354
+
355
+ /************************************************************
356
+ * EV_REPLACE_PAGE { id, title, content, discardable? }
357
+ ************************************************************/
358
+ function ac_replace_page(gobj, event, kw, src)
359
+ {
360
+ let priv = gobj.priv;
361
+
362
+ let resolved = resolve_panel(kw.content);
363
+ if(!resolved.$el) {
364
+ log_error(`${GCLASS_NAME}: EV_REPLACE_PAGE without content`);
365
+ return -1;
366
+ }
367
+
368
+ let $body = gobj_read_attr(gobj, "$container").querySelector('.yui-pager-body');
369
+
370
+ let old = priv.stack.pop();
371
+ if(old && old.$el && old.$el.parentNode) {
372
+ old.$el.parentNode.removeChild(old.$el);
373
+ }
374
+
375
+ resolved.$el.classList.remove('is-hidden');
376
+ if(resolved.$el.parentNode !== $body) {
377
+ $body.appendChild(resolved.$el);
378
+ }
379
+
380
+ priv.stack.push({
381
+ id: kw.id || "",
382
+ title: kw.title || "",
383
+ discardable: !!kw.discardable,
384
+ $el: resolved.$el,
385
+ gobj: resolved.gobj,
386
+ });
387
+
388
+ render_header(gobj);
389
+ gobj_publish_event(gobj, "EV_PAGE_SHOWN", {id: kw.id || "", depth: priv.stack.length});
390
+ return 0;
391
+ }
392
+
393
+ /************************************************************
394
+ * EV_DISCARD_PAGE — emit EV_PAGE_DISCARD for the top page
395
+ ************************************************************/
396
+ function ac_discard_page(gobj, event, kw, src)
397
+ {
398
+ let priv = gobj.priv;
399
+ let top = priv.stack[priv.stack.length - 1];
400
+ if(top) {
401
+ gobj_publish_event(gobj, "EV_PAGE_DISCARD", {id: top.id});
402
+ }
403
+ return 0;
404
+ }
405
+
406
+ /************************************************************
407
+ * EV_SHOW — parent (routing/host) shows us
408
+ ************************************************************/
409
+ function ac_show(gobj, event, kw, src)
410
+ {
411
+ let $container = gobj_read_attr(gobj, "$container");
412
+ if($container) {
413
+ $container.classList.remove('is-hidden');
414
+ }
415
+ forward_top(gobj, "EV_SHOW", kw);
416
+ return 0;
417
+ }
418
+
419
+ /************************************************************
420
+ * EV_HIDE — parent (routing/host) hides us
421
+ ************************************************************/
422
+ function ac_hide(gobj, event, kw, src)
423
+ {
424
+ let $container = gobj_read_attr(gobj, "$container");
425
+ if($container) {
426
+ $container.classList.add('is-hidden');
427
+ }
428
+ forward_top(gobj, "EV_HIDE", {});
429
+ return 0;
430
+ }
431
+
432
+ /************************************************************
433
+ * EV_REFRESH — re-render chrome and forward to the top page
434
+ ************************************************************/
435
+ function ac_refresh(gobj, event, kw, src)
436
+ {
437
+ render_header(gobj);
438
+ forward_top(gobj, "EV_REFRESH", kw);
439
+ return 0;
440
+ }
441
+
442
+ /************************************************************
443
+ * EV_RESIZE — forward to the top page
444
+ ************************************************************/
445
+ function ac_resize(gobj, event, kw, src)
446
+ {
447
+ forward_top(gobj, "EV_RESIZE", kw);
448
+ return 0;
449
+ }
450
+
451
+
452
+
453
+
454
+ /***************************
455
+ * FSM
456
+ ***************************/
457
+
458
+
459
+
460
+
461
+ /*---------------------------------------------*
462
+ * Global methods table
463
+ *---------------------------------------------*/
464
+ const gmt = {
465
+ mt_create: mt_create,
466
+ mt_start: mt_start,
467
+ mt_stop: mt_stop,
468
+ mt_destroy: mt_destroy,
469
+ };
470
+
471
+ /***************************************************************
472
+ * Create the GClass
473
+ ***************************************************************/
474
+ function create_gclass(gclass_name)
475
+ {
476
+ if(__gclass__) {
477
+ log_error(`GClass ALREADY created: ${gclass_name}`);
478
+ return -1;
479
+ }
480
+
481
+ /*---------------------------------------------*
482
+ * States
483
+ *---------------------------------------------*/
484
+ const states = [
485
+ ["ST_IDLE", [
486
+ ["EV_PUSH_PAGE", ac_push_page, null],
487
+ ["EV_POP_PAGE", ac_pop_page, null],
488
+ ["EV_BACK", ac_pop_page, null],
489
+ ["EV_REPLACE_PAGE", ac_replace_page, null],
490
+ ["EV_DISCARD_PAGE", ac_discard_page, null],
491
+ ["EV_SHOW", ac_show, null],
492
+ ["EV_HIDE", ac_hide, null],
493
+ ["EV_REFRESH", ac_refresh, null],
494
+ ["EV_RESIZE", ac_resize, null]
495
+ ]]
496
+ ];
497
+
498
+ /*---------------------------------------------*
499
+ * Events
500
+ *---------------------------------------------*/
501
+ const event_types = [
502
+ ["EV_PUSH_PAGE", 0],
503
+ ["EV_POP_PAGE", 0],
504
+ ["EV_BACK", 0],
505
+ ["EV_REPLACE_PAGE", 0],
506
+ ["EV_DISCARD_PAGE", 0],
507
+ ["EV_SHOW", 0],
508
+ ["EV_HIDE", 0],
509
+ ["EV_REFRESH", 0],
510
+ ["EV_RESIZE", 0],
511
+ ["EV_PAGE_SHOWN", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
512
+ ["EV_PAGE_DISCARD", event_flag_t.EVF_OUTPUT_EVENT],
513
+ ["EV_PAGER_EXIT", event_flag_t.EVF_OUTPUT_EVENT]
514
+ ];
515
+
516
+ __gclass__ = gclass_create(
517
+ gclass_name,
518
+ event_types,
519
+ states,
520
+ gmt,
521
+ 0, // lmt,
522
+ attrs_table,
523
+ PRIVATE_DATA,
524
+ 0, // authz_table,
525
+ 0, // command_table,
526
+ 0, // s_user_trace_level
527
+ 0 // gclass_flag
528
+ );
529
+
530
+ if(!__gclass__) {
531
+ return -1;
532
+ }
533
+
534
+ return 0;
535
+ }
536
+
537
+ /***************************************************************
538
+ * Register GClass
539
+ ***************************************************************/
540
+ function register_c_yui_pager()
541
+ {
542
+ return create_gclass(GCLASS_NAME);
543
+ }
544
+
545
+ export { register_c_yui_pager };
@@ -1,7 +1,7 @@
1
1
  /*********************************************************
2
2
  * Layout ui_route
3
3
  *********************************************************/
4
- .container-grid-2col {
4
+ .C_YUI_ROUTING {
5
5
  display: grid;
6
6
  grid-template-columns: max-content 1fr;
7
7
  height: 100%;
@@ -301,7 +301,7 @@ function build_layout(gobj)
301
301
  * Layout Schema
302
302
  *----------------------------------------------*/
303
303
  const $container = createElement2(
304
- ['div', {class: 'container-grid-2col', style: ''}, [
304
+ ['div', {class: 'C_YUI_ROUTING', style: ''}, [
305
305
  ['div', {class: 'container-grid-2col-left'},
306
306
  ['aside', {class: 'menu'}, [
307
307
  ['ul', {class: 'menu-list'}, []]