@yuneta/gobj-ui 7.23.64 → 7.23.66
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/README.md +55 -0
- package/dist/gobj-ui.cjs.js +1372 -187
- package/dist/gobj-ui.es.js +1372 -187
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +1185 -254
- package/src/c_yui_shell.js +78 -7
- package/src/c_yui_treedb_graph.js +38 -0
- package/src/shell_toolbar_helpers.js +12 -2
- package/src/shell_toolbar_helpers.test.js +55 -1
- package/src/treedb_fold_model.js +592 -0
- package/src/treedb_fold_model.test.js +308 -0
package/src/c_yui_shell.js
CHANGED
|
@@ -1586,6 +1586,20 @@ function safe_id(s)
|
|
|
1586
1586
|
* drawer { type:"drawer", op:"toggle"|"open"|"close", menu_id? }
|
|
1587
1587
|
* event { type:"event", event, kw? }
|
|
1588
1588
|
* dropdown { type:"dropdown", items[] } (toolbar-only)
|
|
1589
|
+
* link { type:"link", url, target? }
|
|
1590
|
+
*
|
|
1591
|
+
* `link` leaves the application, so on a toolbar item and on the brand
|
|
1592
|
+
* it is rendered as a real <a href> rather than a <button>:
|
|
1593
|
+
* middle-click, open-in-new-tab and the url in the status bar are what
|
|
1594
|
+
* make a link recognisable as one, and a button that assigns
|
|
1595
|
+
* location.href has none of them. It is also why it does not become
|
|
1596
|
+
* an FSM event -- there is no state of this application to transition;
|
|
1597
|
+
* the page is being left. A target of "_blank" gets
|
|
1598
|
+
* rel="noopener noreferrer" for free.
|
|
1599
|
+
*
|
|
1600
|
+
* INSIDE A DROPDOWN it stays a <button>, deliberately: those rows are
|
|
1601
|
+
* role="menuitemradio" in a menu, and a link is not a choice among
|
|
1602
|
+
* options. There the dispatcher navigates.
|
|
1589
1603
|
*
|
|
1590
1604
|
* Per-item `show_on` is honoured: each rendered item is wrapped with
|
|
1591
1605
|
* the same Bulma-helper logic as zones.
|
|
@@ -1703,6 +1717,35 @@ function badge_node(value)
|
|
|
1703
1717
|
return ["span", attrs, text];
|
|
1704
1718
|
}
|
|
1705
1719
|
|
|
1720
|
+
/************************************************************
|
|
1721
|
+
* The attributes that turn a toolbar item into a real link, or
|
|
1722
|
+
* null when this item is not one. Answering null is what keeps
|
|
1723
|
+
* every caller's default (a <button> that dispatches an action)
|
|
1724
|
+
* exactly as it was.
|
|
1725
|
+
************************************************************/
|
|
1726
|
+
function toolbar_link_attrs(it)
|
|
1727
|
+
{
|
|
1728
|
+
let action = (it && it.action) || {};
|
|
1729
|
+
if(action.type !== "link") {
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
if(empty_string(action.url)) {
|
|
1733
|
+
log_warning(
|
|
1734
|
+
`C_YUI_SHELL: toolbar item '${it.id||"?"}' has action.type "link" ` +
|
|
1735
|
+
`without a url`
|
|
1736
|
+
);
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
let attrs = {href: action.url};
|
|
1740
|
+
if(!empty_string(action.target)) {
|
|
1741
|
+
attrs.target = action.target;
|
|
1742
|
+
if(action.target === "_blank") {
|
|
1743
|
+
attrs.rel = "noopener noreferrer";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return attrs;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1706
1749
|
/************************************************************
|
|
1707
1750
|
* Renderer for the default ("action") item kind.
|
|
1708
1751
|
************************************************************/
|
|
@@ -1751,13 +1794,20 @@ function build_toolbar_action_item(gobj, it)
|
|
|
1751
1794
|
btn_attrs.title = tip;
|
|
1752
1795
|
btn_attrs["data-i18n-title"] = tip;
|
|
1753
1796
|
}
|
|
1797
|
+
let link = toolbar_link_attrs(it);
|
|
1798
|
+
if(link) {
|
|
1799
|
+
delete btn_attrs.type; /* meaningless, and invalid, on an <a> */
|
|
1800
|
+
Object.assign(btn_attrs, link);
|
|
1801
|
+
}
|
|
1754
1802
|
let $item = createElement2(
|
|
1755
|
-
["button", btn_attrs, children]
|
|
1803
|
+
[link? "a": "button", btn_attrs, children]
|
|
1756
1804
|
);
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1805
|
+
if(!link) {
|
|
1806
|
+
$item.addEventListener("click", ev => {
|
|
1807
|
+
ev.preventDefault();
|
|
1808
|
+
handle_toolbar_action(gobj, it, $item);
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1761
1811
|
attach_context_action(gobj, $item, it);
|
|
1762
1812
|
return $item;
|
|
1763
1813
|
}
|
|
@@ -1793,7 +1843,11 @@ function build_toolbar_brand_item(gobj, it)
|
|
|
1793
1843
|
attrs["aria-haspopup"] = "menu";
|
|
1794
1844
|
attrs["aria-expanded"] = "false";
|
|
1795
1845
|
}
|
|
1796
|
-
|
|
1846
|
+
let link = toolbar_link_attrs(it);
|
|
1847
|
+
if(link) {
|
|
1848
|
+
tag = "a";
|
|
1849
|
+
Object.assign(attrs, link);
|
|
1850
|
+
} else if(action_type === "") {
|
|
1797
1851
|
/* Passive brand: no action — render a div so it is not
|
|
1798
1852
|
* keyboard-focused and screen readers don't announce a
|
|
1799
1853
|
* pressable control. */
|
|
@@ -1807,7 +1861,7 @@ function build_toolbar_brand_item(gobj, it)
|
|
|
1807
1861
|
["span", {class: "yui-toolbar-brand-wordmark", i18n: it.wordmark},
|
|
1808
1862
|
it.wordmark]
|
|
1809
1863
|
]]);
|
|
1810
|
-
if(action_type !== "") {
|
|
1864
|
+
if(action_type !== "" && !link) {
|
|
1811
1865
|
$item.addEventListener("click", ev => {
|
|
1812
1866
|
ev.preventDefault();
|
|
1813
1867
|
handle_toolbar_action(gobj, it, $item);
|
|
@@ -2032,6 +2086,23 @@ function handle_toolbar_action(gobj, item, $trigger)
|
|
|
2032
2086
|
case "dropdown":
|
|
2033
2087
|
toggle_toolbar_dropdown(gobj, item, action, $trigger);
|
|
2034
2088
|
break;
|
|
2089
|
+
case "link":
|
|
2090
|
+
/* Reached only from the renderers that stay buttons (avatar,
|
|
2091
|
+
* and the entries inside a dropdown panel). The anchor
|
|
2092
|
+
* renderers never come through here. */
|
|
2093
|
+
if(!empty_string(action.url)) {
|
|
2094
|
+
if(action.target === "_blank") {
|
|
2095
|
+
window.open(action.url, "_blank", "noopener,noreferrer");
|
|
2096
|
+
} else {
|
|
2097
|
+
window.location.assign(action.url);
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
log_warning(
|
|
2101
|
+
`C_YUI_SHELL: toolbar item '${item.id||"?"}' has action.type ` +
|
|
2102
|
+
`"link" without a url`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
break;
|
|
2035
2106
|
default:
|
|
2036
2107
|
log_warning(
|
|
2037
2108
|
`C_YUI_SHELL: toolbar item '${item.id||"?"}' has no/unknown action.type`
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
kw_get_dict_value,
|
|
38
38
|
gobj_short_name,
|
|
39
39
|
gobj_read_str_attr,
|
|
40
|
+
gobj_read_integer_attr,
|
|
40
41
|
gobj_destroy,
|
|
41
42
|
json_object_update,
|
|
42
43
|
is_object,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
81
82
|
import {attach_clear} from "./yui_inputs.js";
|
|
82
83
|
import {register_c_g6_nodes_tree} from "./c_g6_nodes_tree.js";
|
|
84
|
+
import {yui_graph_fold_items} from "./yui_graph_camera.js";
|
|
83
85
|
import {
|
|
84
86
|
removeChildElements,
|
|
85
87
|
disableElements,
|
|
@@ -107,6 +109,8 @@ SDATA(data_type_t.DTP_LIST, "operation_modes", 0,
|
|
|
107
109
|
'["reading", "operation", "writing", "edition"]',
|
|
108
110
|
"Available **permission** or behaviour modes. These operation modes are required to be accomplish by the graph handler (G6 child). TODO permissions must match treedb permissions."),
|
|
109
111
|
SDATA(data_type_t.DTP_BOOLEAN, "with_treedb_tables",0, false, "Include treedb tables"),
|
|
112
|
+
SDATA(data_type_t.DTP_INTEGER, "expand_depth", 0, 2, "Levels of the treedb open when it loads: 1 = the roots alone, 2 = the roots and their children. Forwarded to the G6 child"),
|
|
113
|
+
SDATA(data_type_t.DTP_INTEGER, "fold_page_size", 0, 24, "Children of one hook shown per page in the graph; a `+N` chip opens the next page. Forwarded to the G6 child"),
|
|
110
114
|
|
|
111
115
|
/*---------------- User last selections ----------------*/
|
|
112
116
|
SDATA(data_type_t.DTP_STRING, "operation_mode", sdata_flag_t.SDF_PERSIST, "reading", "Current operation mode (internal behaviour or role). Changed by the user trough the gui."),
|
|
@@ -240,6 +244,8 @@ function mt_create(gobj)
|
|
|
240
244
|
with_treedb_tables: priv.with_treedb_tables,
|
|
241
245
|
hook_port_position: "bottom",
|
|
242
246
|
fkey_port_position: "top",
|
|
247
|
+
expand_depth: gobj_read_integer_attr(gobj, "expand_depth"),
|
|
248
|
+
fold_page_size: gobj_read_integer_attr(gobj, "fold_page_size"),
|
|
243
249
|
},
|
|
244
250
|
gobj
|
|
245
251
|
);
|
|
@@ -563,6 +569,12 @@ function make_toolbar(gobj)
|
|
|
563
569
|
}
|
|
564
570
|
}],
|
|
565
571
|
|
|
572
|
+
/* The fold pair, the same two chevrons the JSON graph and the
|
|
573
|
+
* lazy tree use: the graph opens folded (see the G6 child), and
|
|
574
|
+
* these are the whole thing and the roots alone. Refresh is the
|
|
575
|
+
* way back to the default depth. */
|
|
576
|
+
...yui_graph_fold_items(gobj, gobj_read_str_attr(gobj, "wide")),
|
|
577
|
+
|
|
566
578
|
/* Which colour is which topic. The port colour of a node encodes
|
|
567
579
|
* the topic it links to, which is the whole point of the graph and
|
|
568
580
|
* which nothing on screen explained. Clicking an entry focuses that
|
|
@@ -2466,6 +2478,28 @@ function ac_find_nodes(gobj, event, kw, src)
|
|
|
2466
2478
|
return 0;
|
|
2467
2479
|
}
|
|
2468
2480
|
|
|
2481
|
+
/************************************************************
|
|
2482
|
+
* The fold pair of the toolbar, forwarded to the G6 child,
|
|
2483
|
+
* which owns the tree.
|
|
2484
|
+
************************************************************/
|
|
2485
|
+
function ac_expand_all(gobj, event, kw, src)
|
|
2486
|
+
{
|
|
2487
|
+
let priv = gobj.priv;
|
|
2488
|
+
if(priv.gobj_nodes_tree) {
|
|
2489
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_EXPAND_ALL", {}, gobj);
|
|
2490
|
+
}
|
|
2491
|
+
return 0;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function ac_collapse_all(gobj, event, kw, src)
|
|
2495
|
+
{
|
|
2496
|
+
let priv = gobj.priv;
|
|
2497
|
+
if(priv.gobj_nodes_tree) {
|
|
2498
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_COLLAPSE_ALL", {}, gobj);
|
|
2499
|
+
}
|
|
2500
|
+
return 0;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2469
2503
|
/************************************************************
|
|
2470
2504
|
* How many nodes the term matched, from the graph child. Zero with a
|
|
2471
2505
|
* term typed is an ANSWER and is shown; an empty box shows nothing.
|
|
@@ -2602,6 +2636,8 @@ function create_gclass(gclass_name)
|
|
|
2602
2636
|
["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
|
|
2603
2637
|
["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
|
|
2604
2638
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
2639
|
+
["EV_EXPAND_ALL", ac_expand_all, null],
|
|
2640
|
+
["EV_COLLAPSE_ALL", ac_collapse_all, null],
|
|
2605
2641
|
["EV_LAYOUT_AUTOSET", ac_layout_autoset, null],
|
|
2606
2642
|
["EV_TOGGLE_LEGEND", ac_toggle_legend, null],
|
|
2607
2643
|
["EV_LEGEND_TOPIC", ac_legend_topic, null],
|
|
@@ -2641,6 +2677,8 @@ function create_gclass(gclass_name)
|
|
|
2641
2677
|
["EV_SET_OPERATION_MODE", 0],
|
|
2642
2678
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
2643
2679
|
["EV_FIND_NODES", 0],
|
|
2680
|
+
["EV_EXPAND_ALL", 0],
|
|
2681
|
+
["EV_COLLAPSE_ALL", 0],
|
|
2644
2682
|
["EV_FIND_RESULT", 0],
|
|
2645
2683
|
["EV_LAYOUT_AUTOSET", 0],
|
|
2646
2684
|
["EV_TOGGLE_LEGEND", 0],
|
|
@@ -19,14 +19,19 @@
|
|
|
19
19
|
* triggers an action.type on click
|
|
20
20
|
*
|
|
21
21
|
* Action types accepted on a toolbar item or on a dropdown sub-item:
|
|
22
|
-
* "navigate" | "drawer" | "event" | "dropdown"
|
|
22
|
+
* "navigate" | "drawer" | "event" | "dropdown" | "link"
|
|
23
|
+
*
|
|
24
|
+
* "link" is the one that leaves the application, and the one item
|
|
25
|
+
* rendered as a real <a href> rather than a <button>.
|
|
23
26
|
*
|
|
24
27
|
* Copyright (c) 2026, ArtGins.
|
|
25
28
|
* All Rights Reserved.
|
|
26
29
|
***********************************************************************/
|
|
27
30
|
|
|
28
31
|
export const TOOLBAR_ITEM_KINDS = ["brand", "avatar", "connection", "action"];
|
|
29
|
-
export const TOOLBAR_ACTION_TYPES = [
|
|
32
|
+
export const TOOLBAR_ACTION_TYPES = [
|
|
33
|
+
"navigate", "drawer", "event", "dropdown", "link"
|
|
34
|
+
];
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
/************************************************************
|
|
@@ -141,6 +146,11 @@ export function validate_action(action, owner_id)
|
|
|
141
146
|
`toolbar item '${id}' (action.type:event) requires 'event' name`
|
|
142
147
|
);
|
|
143
148
|
}
|
|
149
|
+
if(action.type === "link" && !_is_non_empty_string(action.url)) {
|
|
150
|
+
warnings.push(
|
|
151
|
+
`toolbar item '${id}' (action.type:link) requires 'url'`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
144
154
|
if(action.type === "dropdown") {
|
|
145
155
|
let nested = validate_dropdown_action(action, id);
|
|
146
156
|
for(let w of nested) {
|
|
@@ -203,7 +203,9 @@ test("dropdown: invalid sub-action surfaces underlying warning", () => {
|
|
|
203
203
|
*============================================================*/
|
|
204
204
|
test("kinds + action-type constants are exposed", () => {
|
|
205
205
|
expect(TOOLBAR_ITEM_KINDS).toEqual(["brand", "avatar", "connection", "action"]);
|
|
206
|
-
expect(TOOLBAR_ACTION_TYPES).toEqual(
|
|
206
|
+
expect(TOOLBAR_ACTION_TYPES).toEqual(
|
|
207
|
+
["navigate", "drawer", "event", "dropdown", "link"]
|
|
208
|
+
);
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
|
|
@@ -246,3 +248,55 @@ test("dropdown: non-boolean 'selected' warns", () => {
|
|
|
246
248
|
expect(r.length).toBe(1);
|
|
247
249
|
expect(r[0]).toContain("non-boolean");
|
|
248
250
|
});
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/*============================================================
|
|
254
|
+
* validate_action — link
|
|
255
|
+
*
|
|
256
|
+
* The action that LEAVES the application. It is the only one
|
|
257
|
+
* rendered as an <a href>, so the url is not an optional extra:
|
|
258
|
+
* without it there is no link, only a dead control.
|
|
259
|
+
*============================================================*/
|
|
260
|
+
test("validate link: ok with a url", () => {
|
|
261
|
+
let r = validate_toolbar_item({
|
|
262
|
+
id: "landing", icon: "yi-arrow-left", name: "yuneta.io",
|
|
263
|
+
action: { type: "link", url: "https://yuneta.io/" }
|
|
264
|
+
});
|
|
265
|
+
expect(r.ok).toBe(true);
|
|
266
|
+
expect(r.warnings).toEqual([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("validate link: a target is optional", () => {
|
|
270
|
+
let r = validate_toolbar_item({
|
|
271
|
+
id: "docs",
|
|
272
|
+
action: { type: "link", url: "https://doc.yuneta.io/", target: "_blank" }
|
|
273
|
+
});
|
|
274
|
+
expect(r.ok).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("validate link: missing url flags warning", () => {
|
|
278
|
+
let r = validate_toolbar_item({
|
|
279
|
+
id: "landing", action: { type: "link" }
|
|
280
|
+
});
|
|
281
|
+
expect(r.ok).toBe(false);
|
|
282
|
+
expect(r.warnings.join("\n")).toMatch(/requires 'url'/);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("validate link: empty url counts as missing", () => {
|
|
286
|
+
let r = validate_toolbar_item({
|
|
287
|
+
id: "landing", action: { type: "link", url: " " }
|
|
288
|
+
});
|
|
289
|
+
expect(r.ok).toBe(false);
|
|
290
|
+
expect(r.warnings.join("\n")).toMatch(/requires 'url'/);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("validate link: accepted inside a dropdown too", () => {
|
|
294
|
+
let r = validate_dropdown_action({
|
|
295
|
+
type: "dropdown",
|
|
296
|
+
items: [
|
|
297
|
+
{ id: "site", name: "yuneta.io",
|
|
298
|
+
action: { type: "link", url: "https://yuneta.io/" } },
|
|
299
|
+
],
|
|
300
|
+
}, "help");
|
|
301
|
+
expect(r).toEqual([]);
|
|
302
|
+
});
|