@yuneta/gobj-ui 5.2.0 → 5.3.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.
- package/README.md +165 -0
- package/dist/gobj-ui.cjs.js +386 -43
- package/dist/gobj-ui.es.js +520 -177
- package/package.json +1 -1
- package/src/c_yui_json.css +23 -3
- package/src/c_yui_json.js +17 -3
- package/src/c_yui_nav.js +63 -3
- package/src/c_yui_node.css +77 -0
- package/src/c_yui_node.js +1567 -0
- package/src/c_yui_shell.css +18 -0
- package/src/c_yui_shell.js +206 -4
- package/src/c_yui_treedb_schema.js +1 -0
- package/src/c_yui_window.js +8 -3
- package/src/node_tree_model.js +463 -0
- package/src/node_tree_model.test.js +282 -0
- package/src/route_map_model.js +48 -0
- package/src/route_map_model.test.js +70 -0
- package/src/route_resolver.js +11 -0
- package/src/shell_route_map.css +36 -2
- package/src/shell_route_map.js +194 -45
- package/src/yui_dev.js +3 -3
- package/src/yui_icons.css +13 -0
package/src/c_yui_shell.css
CHANGED
|
@@ -271,6 +271,24 @@
|
|
|
271
271
|
opacity: 0.5;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/*--- breadcrumb: the path as one line (C_YUI_NODE's `projection.path`) ---*/
|
|
275
|
+
.yui-nav-breadcrumb {
|
|
276
|
+
padding: 0.35rem 0.75rem;
|
|
277
|
+
}
|
|
278
|
+
.yui-nav-breadcrumb .yui-nav-crumb {
|
|
279
|
+
display: inline-flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 0.35rem;
|
|
282
|
+
}
|
|
283
|
+
/* Bulma marks the last crumb with li.is-active and greys it; the
|
|
284
|
+
* active_route highlight lands there, so "where I am" reads as the end
|
|
285
|
+
* of the trail instead of as one more link. */
|
|
286
|
+
.yui-nav-breadcrumb li.is-active > a {
|
|
287
|
+
color: var(--bulma-text-strong, #363636);
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
pointer-events: none;
|
|
290
|
+
}
|
|
291
|
+
|
|
274
292
|
/*--- backbar (mobile "← <section>" back-to-index, submenu.index) ---*/
|
|
275
293
|
.yui-nav-backbar {
|
|
276
294
|
display: flex;
|
package/src/c_yui_shell.js
CHANGED
|
@@ -100,6 +100,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outp
|
|
|
100
100
|
SDATA(data_type_t.DTP_JSON, "config", 0, null, "Shell declarative config (zones, menu, stages, toolbar)"),
|
|
101
101
|
SDATA(data_type_t.DTP_STRING, "default_route", 0, "", "Fallback route if hash is empty"),
|
|
102
102
|
SDATA(data_type_t.DTP_STRING, "current_route", 0, "", "Current active route"),
|
|
103
|
+
SDATA(data_type_t.DTP_BOOLEAN, "remember_section_position", 0, false, "A menu click returns to the last position visited inside that section"),
|
|
103
104
|
SDATA(data_type_t.DTP_BOOLEAN, "use_hash", 0, true, "Bind navigation to window.location.hash"),
|
|
104
105
|
SDATA(data_type_t.DTP_POINTER, "mount_element", 0, null, "HTMLElement to mount shell into (default: document.body)"),
|
|
105
106
|
|
|
@@ -113,6 +114,9 @@ let PRIVATE_DATA = {
|
|
|
113
114
|
stages: {},
|
|
114
115
|
navs: [],
|
|
115
116
|
item_index: {},
|
|
117
|
+
/* Section route -> last position visited inside it. Mirrors the
|
|
118
|
+
* url for the duration of the page; see remember_position(). */
|
|
119
|
+
section_pos: {},
|
|
116
120
|
/* Sub-route contributor registry (ROUTING.md): a mounted view
|
|
117
121
|
* declares the deep, view-owned children of its base route
|
|
118
122
|
* (topics, /info, /schema, focus topics — subpaths that are NOT
|
|
@@ -225,6 +229,14 @@ function mt_start(gobj)
|
|
|
225
229
|
return;
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
/* Declarative sugar over the attr, which stays the runtime truth:
|
|
233
|
+
* an app can flip it later without rebuilding the shell. */
|
|
234
|
+
let shell_cfg = config.shell || {};
|
|
235
|
+
if(typeof shell_cfg.remember_section_position === "boolean") {
|
|
236
|
+
gobj_write_attr(gobj, "remember_section_position",
|
|
237
|
+
shell_cfg.remember_section_position);
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
build_item_index(gobj, config);
|
|
229
241
|
instantiate_menus(gobj, config);
|
|
230
242
|
build_toolbar(gobj, config);
|
|
@@ -838,6 +850,76 @@ function build_item_index(gobj, config)
|
|
|
838
850
|
};
|
|
839
851
|
}
|
|
840
852
|
}
|
|
853
|
+
|
|
854
|
+
index_node_tree(gobj, config);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/************************************************************
|
|
858
|
+
* config.shell.tree — the navigation is a TREE OF NODES and
|
|
859
|
+
* its root is where the shell's own root used to be.
|
|
860
|
+
*
|
|
861
|
+
* The shell keeps what it is good at, the SPACE: zones, layers,
|
|
862
|
+
* stages, toolbar, overlays, theme, breakpoints. It stops being
|
|
863
|
+
* the owner of the menu: the root C_YUI_NODE holds the primary
|
|
864
|
+
* options as its children and projects them into zones (a left
|
|
865
|
+
* rail, a bottom bar) exactly as `menu.primary.render` used to
|
|
866
|
+
* say — a per-zone projection is still a projection.
|
|
867
|
+
*
|
|
868
|
+
* All this does is synthesize ONE route entry. `owns_subtree`
|
|
869
|
+
* lets it match as the ancestor of everything (route_resolver),
|
|
870
|
+
* so the whole url space below it is the tree's `subpath` and no
|
|
871
|
+
* route table grows. The unknown-route diagnostic is not lost:
|
|
872
|
+
* it moves to the node that knows the names of its children.
|
|
873
|
+
*
|
|
874
|
+
* `menu` and `tree` are not exclusive — an app may declare both
|
|
875
|
+
* while it migrates — but a tree at "/" owns every route no
|
|
876
|
+
* other entry claims, which is the point.
|
|
877
|
+
************************************************************/
|
|
878
|
+
function index_node_tree(gobj, config)
|
|
879
|
+
{
|
|
880
|
+
let priv = gobj.priv;
|
|
881
|
+
let tree = (config.shell && config.shell.tree) || null;
|
|
882
|
+
|
|
883
|
+
if(!tree) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if(!is_object(tree)) {
|
|
887
|
+
log_error("C_YUI_SHELL: config.shell.tree must be an object");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
let base_route = normalize_route(tree.base_route || "/");
|
|
892
|
+
|
|
893
|
+
/* JSON has no comments and these configs annotate themselves with
|
|
894
|
+
* sibling `_name_comment` keys (the established idiom — see
|
|
895
|
+
* app_config.json). They are documentation, not attrs: handing
|
|
896
|
+
* them to gobj_create is an "Attribute NOT FOUND" per comment. */
|
|
897
|
+
let kw = {};
|
|
898
|
+
for(let key of Object.keys(tree)) {
|
|
899
|
+
if(key.charAt(0) === "_" || key === "stage") {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
kw[key] = tree[key];
|
|
903
|
+
}
|
|
904
|
+
kw.base_route = base_route;
|
|
905
|
+
if(!kw.node_id) {
|
|
906
|
+
kw.node_id = "root";
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
let prev = priv.item_index[base_route];
|
|
910
|
+
priv.item_index[base_route] = {
|
|
911
|
+
item: (prev && prev.item) || null,
|
|
912
|
+
parent_item: (prev && prev.parent_item) || null,
|
|
913
|
+
stage: tree.stage || "main",
|
|
914
|
+
menu_id: (prev && prev.menu_id) || "",
|
|
915
|
+
target: {
|
|
916
|
+
stage: tree.stage || "main",
|
|
917
|
+
gclass: "C_YUI_NODE",
|
|
918
|
+
lifecycle: "keep_alive",
|
|
919
|
+
owns_subtree: true,
|
|
920
|
+
kw: kw
|
|
921
|
+
}
|
|
922
|
+
};
|
|
841
923
|
}
|
|
842
924
|
|
|
843
925
|
/************************************************************
|
|
@@ -1365,6 +1447,7 @@ function navigate_to(gobj, route, depth, no_drain)
|
|
|
1365
1447
|
|
|
1366
1448
|
stage.active_route = matched_route;
|
|
1367
1449
|
gobj_write_attr(gobj, "current_route", route);
|
|
1450
|
+
remember_position(gobj, route);
|
|
1368
1451
|
|
|
1369
1452
|
/* Show/hide secondary navs according to parent item */
|
|
1370
1453
|
update_secondary_nav_visibility(gobj, entry);
|
|
@@ -2234,7 +2317,11 @@ function pop_escape(gobj, handler)
|
|
|
2234
2317
|
* - navigate_to() drains (closes) every registered overlay when the
|
|
2235
2318
|
* RESTING route changes — overlays are transient and do not
|
|
2236
2319
|
* outlive the view they float above. An action route or a
|
|
2237
|
-
* subpath-only move keeps them open.
|
|
2320
|
+
* subpath-only move keeps them open. An overlay registered with
|
|
2321
|
+
* `keep_on_navigate` opts OUT of the drain: it is a NAVIGATION
|
|
2322
|
+
* PANEL, not a thing floating above one view — the site map is
|
|
2323
|
+
* the reference case, where every row click is meant to move the
|
|
2324
|
+
* user with the panel still up.
|
|
2238
2325
|
* - a drained/buried entry is left INERT: overlay_dismissed only
|
|
2239
2326
|
* history.back()s when the marker is the CURRENT history entry
|
|
2240
2327
|
* (adjacent, so the back() is invisible) — never over real route
|
|
@@ -2254,14 +2341,25 @@ function drain_overlays(gobj)
|
|
|
2254
2341
|
if(!priv || !priv.overlay_stack) {
|
|
2255
2342
|
return;
|
|
2256
2343
|
}
|
|
2344
|
+
/* Survivors keep their relative order: the stack is a LIFO and a
|
|
2345
|
+
* panel that outlives the drain must stay where it was, or Escape
|
|
2346
|
+
* would start closing overlays in an order the user never built. */
|
|
2347
|
+
let kept = [];
|
|
2257
2348
|
while(priv.overlay_stack.length > 0) {
|
|
2258
2349
|
let entry = priv.overlay_stack.pop();
|
|
2350
|
+
if(entry.keep_on_navigate) {
|
|
2351
|
+
kept.unshift(entry);
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2259
2354
|
try {
|
|
2260
2355
|
entry.close();
|
|
2261
2356
|
} catch(e) {
|
|
2262
2357
|
log_warning(`C_YUI_SHELL: overlay close on navigation failed: ${e}`);
|
|
2263
2358
|
}
|
|
2264
2359
|
}
|
|
2360
|
+
for(let entry of kept) {
|
|
2361
|
+
priv.overlay_stack.push(entry);
|
|
2362
|
+
}
|
|
2265
2363
|
}
|
|
2266
2364
|
|
|
2267
2365
|
/* A URL rewrite moved the entry `st` tags to `hash`. When `st` is a live
|
|
@@ -2279,7 +2377,7 @@ function retag_overlay_hash(gobj, st, hash)
|
|
|
2279
2377
|
}
|
|
2280
2378
|
}
|
|
2281
2379
|
|
|
2282
|
-
function push_overlay_history(gobj, close)
|
|
2380
|
+
function push_overlay_history(gobj, close, opts)
|
|
2283
2381
|
{
|
|
2284
2382
|
let priv = gobj.priv;
|
|
2285
2383
|
if(!priv || !priv.overlay_stack || !gobj_read_attr(gobj, "use_hash")) {
|
|
@@ -2547,6 +2645,11 @@ function ac_nav_clicked(gobj, event, kw, src)
|
|
|
2547
2645
|
return 0;
|
|
2548
2646
|
}
|
|
2549
2647
|
|
|
2648
|
+
/* A click on a section returns to where the user was inside it.
|
|
2649
|
+
* It stays a PUSH: they chose to go there, so Back must bring them
|
|
2650
|
+
* back — only WHICH spot inside was decided for them. */
|
|
2651
|
+
route = remembered_position(gobj, route);
|
|
2652
|
+
|
|
2550
2653
|
/* When hash routing is on, let the hash drive navigate_to() — that
|
|
2551
2654
|
* way back/forward buttons and programmatic hash changes all flow
|
|
2552
2655
|
* through the same code path. Otherwise call navigate_to directly.
|
|
@@ -2852,6 +2955,77 @@ function register_c_yui_shell()
|
|
|
2852
2955
|
* bottom of the file to keep the skeleton layout intact.
|
|
2853
2956
|
***************************************************************/
|
|
2854
2957
|
|
|
2958
|
+
/************************************************************
|
|
2959
|
+
* Last position inside each section — the routes a MENU CLICK
|
|
2960
|
+
* returns to (`remember_section_position`).
|
|
2961
|
+
*
|
|
2962
|
+
* A section is a menu item's route; anything under it is a
|
|
2963
|
+
* position inside it. The memory MIRRORS the url and is never
|
|
2964
|
+
* the authority for it (ROUTING.md §3): it lives for the page,
|
|
2965
|
+
* not on disk, and only a click on the section's own menu item
|
|
2966
|
+
* consults it. Typing the url, a deep link, Back/Forward and
|
|
2967
|
+
* programmatic navigation all land exactly where they say —
|
|
2968
|
+
* which is the property that would be lost if this were stored
|
|
2969
|
+
* and applied everywhere.
|
|
2970
|
+
************************************************************/
|
|
2971
|
+
function remember_position(gobj, route)
|
|
2972
|
+
{
|
|
2973
|
+
let priv = gobj.priv;
|
|
2974
|
+
|
|
2975
|
+
if(!gobj_read_attr(gobj, "remember_section_position")) {
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
for(let section of Object.keys(priv.item_index)) {
|
|
2979
|
+
if(section === "/") {
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
/* The section's own root IS a position inside it. Recording it
|
|
2983
|
+
* is what keeps the memory a MIRROR: after deliberately resting
|
|
2984
|
+
* at /cards, a click on Cards must land there, not teleport
|
|
2985
|
+
* back into the deep spot visited before. */
|
|
2986
|
+
if(route === section || route.indexOf(section + "/") === 0) {
|
|
2987
|
+
priv.section_pos[section] = route;
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/************************************************************
|
|
2993
|
+
* Where a click on `route` should actually land: the last
|
|
2994
|
+
* position inside it, when there is one and it still resolves.
|
|
2995
|
+
* A remembered route whose node has since disappeared is
|
|
2996
|
+
* dropped rather than followed — the tree is allowed to change
|
|
2997
|
+
* under a memory.
|
|
2998
|
+
************************************************************/
|
|
2999
|
+
function remembered_position(gobj, route)
|
|
3000
|
+
{
|
|
3001
|
+
let priv = gobj.priv;
|
|
3002
|
+
|
|
3003
|
+
if(!gobj_read_attr(gobj, "remember_section_position")) {
|
|
3004
|
+
return route;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
/* Only a click that ENTERS the section from outside it restores a
|
|
3008
|
+
* position. From INSIDE — a backbar going up to the index, a tab
|
|
3009
|
+
* strip, the rail item of the section you are already in — the
|
|
3010
|
+
* click means the route it names and nothing else. Without this,
|
|
3011
|
+
* "← Section index" put the user back on the leaf they were
|
|
3012
|
+
* leaving and the control looked dead. */
|
|
3013
|
+
let current = gobj_read_attr(gobj, "current_route") || "";
|
|
3014
|
+
if(current === route || current.indexOf(route + "/") === 0) {
|
|
3015
|
+
return route;
|
|
3016
|
+
}
|
|
3017
|
+
let last = priv.section_pos[route];
|
|
3018
|
+
if(!last || last === route) {
|
|
3019
|
+
return route;
|
|
3020
|
+
}
|
|
3021
|
+
let r = resolve_route(priv.item_index, last);
|
|
3022
|
+
if(!r || !r.entry || !r.entry.target) {
|
|
3023
|
+
delete priv.section_pos[route];
|
|
3024
|
+
return route;
|
|
3025
|
+
}
|
|
3026
|
+
return last;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
2855
3029
|
/************************************************************
|
|
2856
3030
|
* Resolve the shell that governs `gobj`: the nearest
|
|
2857
3031
|
* C_YUI_SHELL ancestor, else the last shell created on the
|
|
@@ -2871,6 +3045,29 @@ function yui_shell_of(gobj)
|
|
|
2871
3045
|
return __last_shell__;
|
|
2872
3046
|
}
|
|
2873
3047
|
|
|
3048
|
+
/************************************************************
|
|
3049
|
+
* The DOM of a zone, for whoever projects into the space.
|
|
3050
|
+
*
|
|
3051
|
+
* The shell owns the space; a root C_YUI_NODE that projects its
|
|
3052
|
+
* children into a left rail or a bottom bar needs the element to
|
|
3053
|
+
* mount them in. Null (and a warning) for an unknown zone —
|
|
3054
|
+
* silently projecting into nowhere is how a menu disappears with
|
|
3055
|
+
* no explanation.
|
|
3056
|
+
************************************************************/
|
|
3057
|
+
function yui_shell_zone(shell_gobj, zone_id)
|
|
3058
|
+
{
|
|
3059
|
+
if(!shell_gobj || !shell_gobj.priv || !shell_gobj.priv.zones) {
|
|
3060
|
+
log_warning(`C_YUI_SHELL: yui_shell_zone — no shell`);
|
|
3061
|
+
return null;
|
|
3062
|
+
}
|
|
3063
|
+
let $zone = shell_gobj.priv.zones[zone_id];
|
|
3064
|
+
if(!$zone) {
|
|
3065
|
+
log_warning(`C_YUI_SHELL: yui_shell_zone — unknown zone '${zone_id}'`);
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
3068
|
+
return $zone;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2874
3071
|
/************************************************************
|
|
2875
3072
|
* Programmatic navigation.
|
|
2876
3073
|
*
|
|
@@ -3000,9 +3197,13 @@ function yui_shell_pop_escape(shell_gobj, handler)
|
|
|
3000
3197
|
* `close_fn` is what the Back button invokes to tear the overlay down.
|
|
3001
3198
|
* Returns null when history integration is off (no shell / use_hash) —
|
|
3002
3199
|
* callers just skip the paired yui_shell_overlay_dismissed then. */
|
|
3003
|
-
|
|
3200
|
+
/* `opts.keep_on_navigate` — this overlay is a navigation PANEL and
|
|
3201
|
+
* survives a resting-route change (see drain_overlays). Default off:
|
|
3202
|
+
* an overlay is transient, and outliving the view it floats above is
|
|
3203
|
+
* the exception that has to be asked for. */
|
|
3204
|
+
function yui_shell_register_overlay(shell_gobj, close_fn, opts)
|
|
3004
3205
|
{
|
|
3005
|
-
return push_overlay_history(shell_gobj, close_fn);
|
|
3206
|
+
return push_overlay_history(shell_gobj, close_fn, opts);
|
|
3006
3207
|
}
|
|
3007
3208
|
function yui_shell_overlay_dismissed(shell_gobj, overlay)
|
|
3008
3209
|
{
|
|
@@ -3160,6 +3361,7 @@ function yui_shell_close_dropdown(shell_gobj)
|
|
|
3160
3361
|
export {
|
|
3161
3362
|
register_c_yui_shell,
|
|
3162
3363
|
yui_shell_of,
|
|
3364
|
+
yui_shell_zone,
|
|
3163
3365
|
yui_shell_navigate,
|
|
3164
3366
|
yui_shell_nav_map,
|
|
3165
3367
|
yui_shell_set_sub_routes,
|
package/src/c_yui_window.js
CHANGED
|
@@ -107,6 +107,7 @@ SDATA(data_type_t.DTP_POINTER, "focus", 0, null, "Brings focus to the
|
|
|
107
107
|
SDATA(data_type_t.DTP_BOOLEAN, "modal", 0, false, "Enable modal mode"),
|
|
108
108
|
SDATA(data_type_t.DTP_BOOLEAN, "keyboard", 0, true, "Close window on ESC if not modal"),
|
|
109
109
|
SDATA(data_type_t.DTP_BOOLEAN, "back_dismissable", 0, true, "Browser Back closes this window (floating overlays only; ignored when it has a `manager`)"),
|
|
110
|
+
SDATA(data_type_t.DTP_BOOLEAN, "keep_on_navigate", 0, false, "This window is a navigation PANEL: a route change does not close it"),
|
|
110
111
|
SDATA(data_type_t.DTP_POINTER, "back_overlay", 0, null, "Internal: overlay-history entry (Back-button integration)"),
|
|
111
112
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: Window container element"),
|
|
112
113
|
SDATA(data_type_t.DTP_STRING, "window_id", 0, "", "Internal: Window ID"),
|
|
@@ -183,12 +184,16 @@ function mt_create(gobj)
|
|
|
183
184
|
} else if(gobj_read_bool_attr(gobj, "back_dismissable")) {
|
|
184
185
|
/* Floating overlay window (no dock manager): the browser Back
|
|
185
186
|
* button closes it, like a modal/popup. Dock-managed windows
|
|
186
|
-
* are persistent workspace surfaces and are left out
|
|
187
|
-
*
|
|
187
|
+
* are persistent workspace surfaces and are left out — which
|
|
188
|
+
* is also why a navigation panel prefers a manager, and falls
|
|
189
|
+
* back to `keep_on_navigate` when the app has no dock.
|
|
190
|
+
* Retired in mt_destroy (covers every teardown path). */
|
|
188
191
|
let shell = yui_shell_of(gobj);
|
|
189
192
|
if(shell) {
|
|
190
193
|
let overlay = yui_shell_register_overlay(
|
|
191
|
-
shell,
|
|
194
|
+
shell,
|
|
195
|
+
function() { close_window(gobj); },
|
|
196
|
+
{keep_on_navigate: gobj_read_bool_attr(gobj, "keep_on_navigate")}
|
|
192
197
|
);
|
|
193
198
|
gobj_write_attr(gobj, "back_overlay", overlay);
|
|
194
199
|
}
|