@yuneta/gobj-ui 5.2.1 → 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 +385 -41
- package/dist/gobj-ui.es.js +385 -41
- 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_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/dist/gobj-ui.es.js
CHANGED
|
@@ -339,6 +339,11 @@ function _is_non_empty_string(v) {
|
|
|
339
339
|
* - root `/` is NEVER an ancestor catch-all (it only matches
|
|
340
340
|
* exactly): otherwise every typo route would silently mount
|
|
341
341
|
* the home view and the unknown-route diagnostic would die.
|
|
342
|
+
* ONE exception, opt-in: an entry whose target declares
|
|
343
|
+
* `owns_subtree` is a view that resolves the tail ITSELF and
|
|
344
|
+
* says so when a segment names nothing — a C_YUI_NODE tree
|
|
345
|
+
* rooted at `/`. The diagnostic is not lost there, it moves
|
|
346
|
+
* one layer down to whoever actually knows the names.
|
|
342
347
|
* - nothing matched → entry as found (may be
|
|
343
348
|
* null or a targetless submenu parent), subpath "".
|
|
344
349
|
************************************************************/
|
|
@@ -366,7 +371,15 @@ function resolve_route(item_index, route) {
|
|
|
366
371
|
while (parts.length > 0 && (!entry || !entry.target)) {
|
|
367
372
|
parts.pop();
|
|
368
373
|
let cand = "/" + parts.join("/");
|
|
369
|
-
if (cand === "/")
|
|
374
|
+
if (cand === "/") {
|
|
375
|
+
let root = item_index["/"];
|
|
376
|
+
if (root && root.target && root.target.owns_subtree) {
|
|
377
|
+
entry = root;
|
|
378
|
+
matched_route = "/";
|
|
379
|
+
subpath = route.replace(/^\/+/, "");
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
370
383
|
let e = item_index[cand];
|
|
371
384
|
if (e && e.target) {
|
|
372
385
|
entry = e;
|
|
@@ -483,6 +496,38 @@ function collect_routes(nodes, into) {
|
|
|
483
496
|
}
|
|
484
497
|
}
|
|
485
498
|
/************************************************************
|
|
499
|
+
* A route can be reached from more than one surface: the primary
|
|
500
|
+
* menu, a drawer, an account dropdown. Each occurrence rendered
|
|
501
|
+
* its whole SUBTREE, so a branch reachable three ways was drawn
|
|
502
|
+
* three times — bearable when a section had four children, noise
|
|
503
|
+
* once a node tree hangs there with fourteen.
|
|
504
|
+
*
|
|
505
|
+
* Exactly one occurrence owns the subtree; the others become
|
|
506
|
+
* references (`ref: true`, no children) — still real, clickable
|
|
507
|
+
* routes, just not repeated structure.
|
|
508
|
+
*
|
|
509
|
+
* Who owns it: the first occurrence in the NAV, then in `other`,
|
|
510
|
+
* then in the toolbar. The nav is where the app's structure
|
|
511
|
+
* lives; a toolbar entry pointing at the same route is a
|
|
512
|
+
* shortcut — a non-structural edge — and hanging the tree off it
|
|
513
|
+
* would bury the whole structure inside the account menu.
|
|
514
|
+
************************************************************/
|
|
515
|
+
function dedupe_subtrees(groups) {
|
|
516
|
+
let owner = {};
|
|
517
|
+
let visit = (node) => {
|
|
518
|
+
if (node.route) {
|
|
519
|
+
if (owner[node.route] === void 0) owner[node.route] = node;
|
|
520
|
+
else if (owner[node.route] !== node) {
|
|
521
|
+
node.ref = true;
|
|
522
|
+
node.children = [];
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(node.children)) node.children.forEach(visit);
|
|
527
|
+
};
|
|
528
|
+
for (let group of groups) group.forEach(visit);
|
|
529
|
+
}
|
|
530
|
+
/************************************************************
|
|
486
531
|
* Mark "you are here": the node whose route best matches
|
|
487
532
|
* current_route — exact hit wins, else the LONGEST declared
|
|
488
533
|
* route that is a path-prefix of it (the base view of a deep
|
|
@@ -492,6 +537,7 @@ function mark_current(groups, current_route) {
|
|
|
492
537
|
if (!current_route) return;
|
|
493
538
|
let best = null;
|
|
494
539
|
let visit = (n) => {
|
|
540
|
+
if (n.ref) return;
|
|
495
541
|
if (n.route) {
|
|
496
542
|
if (n.route === current_route) {
|
|
497
543
|
if (!best || best.node.route !== current_route) best = {
|
|
@@ -609,6 +655,11 @@ function build_nav_map(input) {
|
|
|
609
655
|
});
|
|
610
656
|
}
|
|
611
657
|
other.forEach(enrich);
|
|
658
|
+
dedupe_subtrees([
|
|
659
|
+
nav,
|
|
660
|
+
other,
|
|
661
|
+
toolbar
|
|
662
|
+
]);
|
|
612
663
|
mark_current([
|
|
613
664
|
toolbar,
|
|
614
665
|
nav,
|
|
@@ -770,6 +821,7 @@ var attrs_table$17 = [
|
|
|
770
821
|
SDATA(data_type_t.DTP_JSON, "config", 0, null, "Shell declarative config (zones, menu, stages, toolbar)"),
|
|
771
822
|
SDATA(data_type_t.DTP_STRING, "default_route", 0, "", "Fallback route if hash is empty"),
|
|
772
823
|
SDATA(data_type_t.DTP_STRING, "current_route", 0, "", "Current active route"),
|
|
824
|
+
SDATA(data_type_t.DTP_BOOLEAN, "remember_section_position", 0, false, "A menu click returns to the last position visited inside that section"),
|
|
773
825
|
SDATA(data_type_t.DTP_BOOLEAN, "use_hash", 0, true, "Bind navigation to window.location.hash"),
|
|
774
826
|
SDATA(data_type_t.DTP_POINTER, "mount_element", 0, null, "HTMLElement to mount shell into (default: document.body)"),
|
|
775
827
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of the shell"),
|
|
@@ -781,6 +833,7 @@ var PRIVATE_DATA$17 = {
|
|
|
781
833
|
stages: {},
|
|
782
834
|
navs: [],
|
|
783
835
|
item_index: {},
|
|
836
|
+
section_pos: {},
|
|
784
837
|
sub_routes: {},
|
|
785
838
|
event_handlers: {},
|
|
786
839
|
hash_handler: null,
|
|
@@ -828,6 +881,8 @@ function mt_start$14(gobj) {
|
|
|
828
881
|
]));
|
|
829
882
|
return;
|
|
830
883
|
}
|
|
884
|
+
let shell_cfg = config.shell || {};
|
|
885
|
+
if (typeof shell_cfg.remember_section_position === "boolean") gobj_write_attr(gobj, "remember_section_position", shell_cfg.remember_section_position);
|
|
831
886
|
build_item_index(gobj, config);
|
|
832
887
|
instantiate_menus(gobj, config);
|
|
833
888
|
build_toolbar(gobj, config);
|
|
@@ -1187,6 +1242,59 @@ function build_item_index(gobj, config) {
|
|
|
1187
1242
|
menu_id: prev && prev.menu_id || ""
|
|
1188
1243
|
};
|
|
1189
1244
|
}
|
|
1245
|
+
index_node_tree(gobj, config);
|
|
1246
|
+
}
|
|
1247
|
+
/************************************************************
|
|
1248
|
+
* config.shell.tree — the navigation is a TREE OF NODES and
|
|
1249
|
+
* its root is where the shell's own root used to be.
|
|
1250
|
+
*
|
|
1251
|
+
* The shell keeps what it is good at, the SPACE: zones, layers,
|
|
1252
|
+
* stages, toolbar, overlays, theme, breakpoints. It stops being
|
|
1253
|
+
* the owner of the menu: the root C_YUI_NODE holds the primary
|
|
1254
|
+
* options as its children and projects them into zones (a left
|
|
1255
|
+
* rail, a bottom bar) exactly as `menu.primary.render` used to
|
|
1256
|
+
* say — a per-zone projection is still a projection.
|
|
1257
|
+
*
|
|
1258
|
+
* All this does is synthesize ONE route entry. `owns_subtree`
|
|
1259
|
+
* lets it match as the ancestor of everything (route_resolver),
|
|
1260
|
+
* so the whole url space below it is the tree's `subpath` and no
|
|
1261
|
+
* route table grows. The unknown-route diagnostic is not lost:
|
|
1262
|
+
* it moves to the node that knows the names of its children.
|
|
1263
|
+
*
|
|
1264
|
+
* `menu` and `tree` are not exclusive — an app may declare both
|
|
1265
|
+
* while it migrates — but a tree at "/" owns every route no
|
|
1266
|
+
* other entry claims, which is the point.
|
|
1267
|
+
************************************************************/
|
|
1268
|
+
function index_node_tree(gobj, config) {
|
|
1269
|
+
let priv = gobj.priv;
|
|
1270
|
+
let tree = config.shell && config.shell.tree || null;
|
|
1271
|
+
if (!tree) return;
|
|
1272
|
+
if (!is_object(tree)) {
|
|
1273
|
+
log_error("C_YUI_SHELL: config.shell.tree must be an object");
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
let base_route = normalize_route(tree.base_route || "/");
|
|
1277
|
+
let kw = {};
|
|
1278
|
+
for (let key of Object.keys(tree)) {
|
|
1279
|
+
if (key.charAt(0) === "_" || key === "stage") continue;
|
|
1280
|
+
kw[key] = tree[key];
|
|
1281
|
+
}
|
|
1282
|
+
kw.base_route = base_route;
|
|
1283
|
+
if (!kw.node_id) kw.node_id = "root";
|
|
1284
|
+
let prev = priv.item_index[base_route];
|
|
1285
|
+
priv.item_index[base_route] = {
|
|
1286
|
+
item: prev && prev.item || null,
|
|
1287
|
+
parent_item: prev && prev.parent_item || null,
|
|
1288
|
+
stage: tree.stage || "main",
|
|
1289
|
+
menu_id: prev && prev.menu_id || "",
|
|
1290
|
+
target: {
|
|
1291
|
+
stage: tree.stage || "main",
|
|
1292
|
+
gclass: "C_YUI_NODE",
|
|
1293
|
+
lifecycle: "keep_alive",
|
|
1294
|
+
owns_subtree: true,
|
|
1295
|
+
kw
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1190
1298
|
}
|
|
1191
1299
|
/************************************************************
|
|
1192
1300
|
* For each menu declared: for each zone hosting it, create
|
|
@@ -1431,6 +1539,7 @@ function navigate_to(gobj, route, depth, no_drain) {
|
|
|
1431
1539
|
if ($c) $c.classList.remove("is-hidden");
|
|
1432
1540
|
stage.active_route = matched_route;
|
|
1433
1541
|
gobj_write_attr(gobj, "current_route", route);
|
|
1542
|
+
remember_position(gobj, route);
|
|
1434
1543
|
update_secondary_nav_visibility(gobj, entry);
|
|
1435
1544
|
close_all_drawers(gobj);
|
|
1436
1545
|
close_toolbar_dropdown(gobj);
|
|
@@ -2055,7 +2164,11 @@ function pop_escape(gobj, handler) {
|
|
|
2055
2164
|
* - navigate_to() drains (closes) every registered overlay when the
|
|
2056
2165
|
* RESTING route changes — overlays are transient and do not
|
|
2057
2166
|
* outlive the view they float above. An action route or a
|
|
2058
|
-
* subpath-only move keeps them open.
|
|
2167
|
+
* subpath-only move keeps them open. An overlay registered with
|
|
2168
|
+
* `keep_on_navigate` opts OUT of the drain: it is a NAVIGATION
|
|
2169
|
+
* PANEL, not a thing floating above one view — the site map is
|
|
2170
|
+
* the reference case, where every row click is meant to move the
|
|
2171
|
+
* user with the panel still up.
|
|
2059
2172
|
* - a drained/buried entry is left INERT: overlay_dismissed only
|
|
2060
2173
|
* history.back()s when the marker is the CURRENT history entry
|
|
2061
2174
|
* (adjacent, so the back() is invisible) — never over real route
|
|
@@ -2068,14 +2181,20 @@ function pop_escape(gobj, handler) {
|
|
|
2068
2181
|
function drain_overlays(gobj) {
|
|
2069
2182
|
let priv = gobj.priv;
|
|
2070
2183
|
if (!priv || !priv.overlay_stack) return;
|
|
2184
|
+
let kept = [];
|
|
2071
2185
|
while (priv.overlay_stack.length > 0) {
|
|
2072
2186
|
let entry = priv.overlay_stack.pop();
|
|
2187
|
+
if (entry.keep_on_navigate) {
|
|
2188
|
+
kept.unshift(entry);
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2073
2191
|
try {
|
|
2074
2192
|
entry.close();
|
|
2075
2193
|
} catch (e) {
|
|
2076
2194
|
log_warning(`C_YUI_SHELL: overlay close on navigation failed: ${e}`);
|
|
2077
2195
|
}
|
|
2078
2196
|
}
|
|
2197
|
+
for (let entry of kept) priv.overlay_stack.push(entry);
|
|
2079
2198
|
}
|
|
2080
2199
|
function retag_overlay_hash(gobj, st, hash) {
|
|
2081
2200
|
let priv = gobj.priv;
|
|
@@ -2083,7 +2202,7 @@ function retag_overlay_hash(gobj, st, hash) {
|
|
|
2083
2202
|
let entry = priv.overlay_stack.find((e) => e.id === st.__yui_overlay__);
|
|
2084
2203
|
if (entry) entry.hash = hash;
|
|
2085
2204
|
}
|
|
2086
|
-
function push_overlay_history(gobj, close) {
|
|
2205
|
+
function push_overlay_history(gobj, close, opts) {
|
|
2087
2206
|
let priv = gobj.priv;
|
|
2088
2207
|
if (!priv || !priv.overlay_stack || !gobj_read_attr(gobj, "use_hash")) return null;
|
|
2089
2208
|
let entry = {
|
|
@@ -2214,6 +2333,7 @@ function update_secondary_nav_visibility(gobj, entry) {
|
|
|
2214
2333
|
function ac_nav_clicked(gobj, event, kw, src) {
|
|
2215
2334
|
let route = kw && kw.route || "";
|
|
2216
2335
|
if (empty_string(route)) return 0;
|
|
2336
|
+
route = remembered_position(gobj, route);
|
|
2217
2337
|
if (gobj_read_attr(gobj, "use_hash") && !route_restores_url(gobj, route)) {
|
|
2218
2338
|
let target_hash = route_to_hash(route);
|
|
2219
2339
|
if (window.location.hash !== target_hash) window.location.hash = target_hash;
|
|
@@ -2297,6 +2417,48 @@ function register_c_yui_shell() {
|
|
|
2297
2417
|
* bottom of the file to keep the skeleton layout intact.
|
|
2298
2418
|
***************************************************************/
|
|
2299
2419
|
/************************************************************
|
|
2420
|
+
* Last position inside each section — the routes a MENU CLICK
|
|
2421
|
+
* returns to (`remember_section_position`).
|
|
2422
|
+
*
|
|
2423
|
+
* A section is a menu item's route; anything under it is a
|
|
2424
|
+
* position inside it. The memory MIRRORS the url and is never
|
|
2425
|
+
* the authority for it (ROUTING.md §3): it lives for the page,
|
|
2426
|
+
* not on disk, and only a click on the section's own menu item
|
|
2427
|
+
* consults it. Typing the url, a deep link, Back/Forward and
|
|
2428
|
+
* programmatic navigation all land exactly where they say —
|
|
2429
|
+
* which is the property that would be lost if this were stored
|
|
2430
|
+
* and applied everywhere.
|
|
2431
|
+
************************************************************/
|
|
2432
|
+
function remember_position(gobj, route) {
|
|
2433
|
+
let priv = gobj.priv;
|
|
2434
|
+
if (!gobj_read_attr(gobj, "remember_section_position")) return;
|
|
2435
|
+
for (let section of Object.keys(priv.item_index)) {
|
|
2436
|
+
if (section === "/") continue;
|
|
2437
|
+
if (route === section || route.indexOf(section + "/") === 0) priv.section_pos[section] = route;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
/************************************************************
|
|
2441
|
+
* Where a click on `route` should actually land: the last
|
|
2442
|
+
* position inside it, when there is one and it still resolves.
|
|
2443
|
+
* A remembered route whose node has since disappeared is
|
|
2444
|
+
* dropped rather than followed — the tree is allowed to change
|
|
2445
|
+
* under a memory.
|
|
2446
|
+
************************************************************/
|
|
2447
|
+
function remembered_position(gobj, route) {
|
|
2448
|
+
let priv = gobj.priv;
|
|
2449
|
+
if (!gobj_read_attr(gobj, "remember_section_position")) return route;
|
|
2450
|
+
let current = gobj_read_attr(gobj, "current_route") || "";
|
|
2451
|
+
if (current === route || current.indexOf(route + "/") === 0) return route;
|
|
2452
|
+
let last = priv.section_pos[route];
|
|
2453
|
+
if (!last || last === route) return route;
|
|
2454
|
+
let r = resolve_route(priv.item_index, last);
|
|
2455
|
+
if (!r || !r.entry || !r.entry.target) {
|
|
2456
|
+
delete priv.section_pos[route];
|
|
2457
|
+
return route;
|
|
2458
|
+
}
|
|
2459
|
+
return last;
|
|
2460
|
+
}
|
|
2461
|
+
/************************************************************
|
|
2300
2462
|
* Resolve the shell that governs `gobj`: the nearest
|
|
2301
2463
|
* C_YUI_SHELL ancestor, else the last shell created on the
|
|
2302
2464
|
* page (apps have exactly one). Null when no shell exists —
|
|
@@ -2398,8 +2560,8 @@ function yui_shell_push_escape(shell_gobj, layer, handler) {
|
|
|
2398
2560
|
function yui_shell_pop_escape(shell_gobj, handler) {
|
|
2399
2561
|
pop_escape(shell_gobj, handler);
|
|
2400
2562
|
}
|
|
2401
|
-
function yui_shell_register_overlay(shell_gobj, close_fn) {
|
|
2402
|
-
return push_overlay_history(shell_gobj, close_fn);
|
|
2563
|
+
function yui_shell_register_overlay(shell_gobj, close_fn, opts) {
|
|
2564
|
+
return push_overlay_history(shell_gobj, close_fn, opts);
|
|
2403
2565
|
}
|
|
2404
2566
|
function yui_shell_overlay_dismissed(shell_gobj, overlay) {
|
|
2405
2567
|
overlay_dismissed(shell_gobj, overlay);
|
|
@@ -3003,6 +3165,7 @@ var attrs_table$16 = [
|
|
|
3003
3165
|
SDATA(data_type_t.DTP_BOOLEAN, "modal", 0, false, "Enable modal mode"),
|
|
3004
3166
|
SDATA(data_type_t.DTP_BOOLEAN, "keyboard", 0, true, "Close window on ESC if not modal"),
|
|
3005
3167
|
SDATA(data_type_t.DTP_BOOLEAN, "back_dismissable", 0, true, "Browser Back closes this window (floating overlays only; ignored when it has a `manager`)"),
|
|
3168
|
+
SDATA(data_type_t.DTP_BOOLEAN, "keep_on_navigate", 0, false, "This window is a navigation PANEL: a route change does not close it"),
|
|
3006
3169
|
SDATA(data_type_t.DTP_POINTER, "back_overlay", 0, null, "Internal: overlay-history entry (Back-button integration)"),
|
|
3007
3170
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: Window container element"),
|
|
3008
3171
|
SDATA(data_type_t.DTP_STRING, "window_id", 0, "", "Internal: Window ID"),
|
|
@@ -3042,7 +3205,7 @@ function mt_create$16(gobj) {
|
|
|
3042
3205
|
let shell = yui_shell_of(gobj);
|
|
3043
3206
|
if (shell) gobj_write_attr(gobj, "back_overlay", yui_shell_register_overlay(shell, function() {
|
|
3044
3207
|
close_window(gobj);
|
|
3045
|
-
}));
|
|
3208
|
+
}, { keep_on_navigate: gobj_read_bool_attr(gobj, "keep_on_navigate") }));
|
|
3046
3209
|
}
|
|
3047
3210
|
let on_win_resize = function() {
|
|
3048
3211
|
handleResize(gobj);
|
|
@@ -10488,10 +10651,19 @@ function leaf_row(key, value, depth) {
|
|
|
10488
10651
|
];
|
|
10489
10652
|
}
|
|
10490
10653
|
/************************************************************
|
|
10491
|
-
* Depth indentation (inline style keeps it CSS-framework free)
|
|
10654
|
+
* Depth indentation (inline style keeps it CSS-framework free).
|
|
10655
|
+
*
|
|
10656
|
+
* FOUR characters per level, house rule: structure is read as
|
|
10657
|
+
* indentation, and it has to be the same four everywhere — this
|
|
10658
|
+
* viewer, the raw dump behind it, the site map's tree. `ch` is
|
|
10659
|
+
* the character width of the row's own font, so the rows line up
|
|
10660
|
+
* with the monospace text they carry instead of drifting from it
|
|
10661
|
+
* at some zoom level.
|
|
10492
10662
|
************************************************************/
|
|
10663
|
+
var INDENT_CH = 4;
|
|
10664
|
+
var INDENT_PAD = .4;
|
|
10493
10665
|
function row_indent(depth) {
|
|
10494
|
-
return "padding-left:" + (
|
|
10666
|
+
return "padding-left:" + (INDENT_PAD + depth * INDENT_CH) + "ch;background-size:" + depth * INDENT_CH + "ch 100%;";
|
|
10495
10667
|
}
|
|
10496
10668
|
/************************************************************
|
|
10497
10669
|
* String() that never throws on a non-primitive
|
|
@@ -10631,7 +10803,7 @@ function ac_collapse_all$1(gobj, event, kw, src) {
|
|
|
10631
10803
|
function ac_copy_all(gobj, event, kw, src) {
|
|
10632
10804
|
let priv = gobj.priv;
|
|
10633
10805
|
if (priv.root === null || priv.root === void 0) return 0;
|
|
10634
|
-
let text = JSON.stringify(priv.root, null,
|
|
10806
|
+
let text = JSON.stringify(priv.root, null, 4);
|
|
10635
10807
|
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).catch(function(e) {
|
|
10636
10808
|
log_error(`${GCLASS_NAME$8}: clipboard write failed: ${e}`);
|
|
10637
10809
|
});
|
|
@@ -12488,6 +12660,13 @@ function cards_grid_descriptor(items, show_label) {
|
|
|
12488
12660
|
* "cards" — grid of tappable cards (section-index landing;
|
|
12489
12661
|
* mounted as a stage view by C_YUI_SHELL when a
|
|
12490
12662
|
* primary item declares submenu.index)
|
|
12663
|
+
* "breadcrumb" — the PATH as one line ("Cards › Energy › South
|
|
12664
|
+
* hall › Meter 3"), each crumb a link to that
|
|
12665
|
+
* level. Unlike every other layout its items
|
|
12666
|
+
* are not a node's children but the trail down
|
|
12667
|
+
* to where the user is: the second way to show
|
|
12668
|
+
* depth, for when stacking one strip per level
|
|
12669
|
+
* costs more vertical room than it is worth.
|
|
12491
12670
|
* "backbar" — single "← <section>" link back to the section
|
|
12492
12671
|
* index; the mobile replacement of the tab strip
|
|
12493
12672
|
* for submenu.index sections (the shell collapses
|
|
@@ -12535,7 +12714,8 @@ var SUPPORTED_LAYOUTS = [
|
|
|
12535
12714
|
"submenu",
|
|
12536
12715
|
"accordion",
|
|
12537
12716
|
"cards",
|
|
12538
|
-
"backbar"
|
|
12717
|
+
"backbar",
|
|
12718
|
+
"breadcrumb"
|
|
12539
12719
|
];
|
|
12540
12720
|
function is_decorative(it) {
|
|
12541
12721
|
return !!(it && (it.type === "header" || it.type === "divider"));
|
|
@@ -12641,6 +12821,9 @@ function build_ui$6(gobj) {
|
|
|
12641
12821
|
case "backbar":
|
|
12642
12822
|
$container = render_backbar(gobj);
|
|
12643
12823
|
break;
|
|
12824
|
+
case "breadcrumb":
|
|
12825
|
+
$container = render_breadcrumb(gobj, items);
|
|
12826
|
+
break;
|
|
12644
12827
|
default: $container = render_vertical(gobj, items);
|
|
12645
12828
|
}
|
|
12646
12829
|
$container.setAttribute("data-nav-zone", zone);
|
|
@@ -12661,6 +12844,7 @@ function build_ui$6(gobj) {
|
|
|
12661
12844
|
}
|
|
12662
12845
|
wire_clicks(gobj, $container);
|
|
12663
12846
|
gobj_write_attr(gobj, "$container", $container);
|
|
12847
|
+
apply_active_route(gobj);
|
|
12664
12848
|
}
|
|
12665
12849
|
/************************************************************
|
|
12666
12850
|
* Rebuild the nav DOM in place from the current menu_items.
|
|
@@ -12679,7 +12863,6 @@ function rebuild(gobj) {
|
|
|
12679
12863
|
if ($new && was_hidden) $new.classList.add("is-hidden");
|
|
12680
12864
|
if (parent && $new) parent.insertBefore($new, next);
|
|
12681
12865
|
if ($old && $old.parentNode) $old.parentNode.removeChild($old);
|
|
12682
|
-
apply_active_route(gobj);
|
|
12683
12866
|
}
|
|
12684
12867
|
/************************************************************
|
|
12685
12868
|
* Re-apply the active-route highlight after a rebuild (the
|
|
@@ -12833,6 +13016,74 @@ function render_cards(gobj, items) {
|
|
|
12833
13016
|
* collapses the whole secondary zone (tabs and cards never
|
|
12834
13017
|
* coexist), so the bar needs no self-hiding logic.
|
|
12835
13018
|
************************************************************/
|
|
13019
|
+
/************************************************************
|
|
13020
|
+
* breadcrumb: the trail to here, one line.
|
|
13021
|
+
*
|
|
13022
|
+
* Bulma's own markup (nav.breadcrumb > ul > li, separators drawn by
|
|
13023
|
+
* CSS), with the SAME data-route/data-item-id contract as every
|
|
13024
|
+
* other layout — so the delegated click handler and the active
|
|
13025
|
+
* highlight work unchanged, and the last crumb marks itself through
|
|
13026
|
+
* `active_route` like a tab does.
|
|
13027
|
+
************************************************************/
|
|
13028
|
+
function render_breadcrumb(gobj, items) {
|
|
13029
|
+
let show_label = gobj_read_attr(gobj, "show_label");
|
|
13030
|
+
let lis = [];
|
|
13031
|
+
for (let it of items || []) {
|
|
13032
|
+
if (is_decorative(it)) continue;
|
|
13033
|
+
let children = [];
|
|
13034
|
+
if (it.icon) children.push([
|
|
13035
|
+
"span",
|
|
13036
|
+
{ class: "icon is-small" },
|
|
13037
|
+
["i", {
|
|
13038
|
+
class: it.icon,
|
|
13039
|
+
"aria-hidden": "true"
|
|
13040
|
+
}]
|
|
13041
|
+
]);
|
|
13042
|
+
if (show_label !== false) {
|
|
13043
|
+
let label = it.name || it.id || "";
|
|
13044
|
+
children.push([
|
|
13045
|
+
"span",
|
|
13046
|
+
{
|
|
13047
|
+
class: "yui-nav-label",
|
|
13048
|
+
i18n: label
|
|
13049
|
+
},
|
|
13050
|
+
label
|
|
13051
|
+
]);
|
|
13052
|
+
}
|
|
13053
|
+
let a_attrs = {
|
|
13054
|
+
class: "yui-nav-item yui-nav-crumb",
|
|
13055
|
+
href: it.route ? "#" + it.route : "#",
|
|
13056
|
+
"data-item-id": it.id,
|
|
13057
|
+
"data-route": it.route || "",
|
|
13058
|
+
"data-disabled": it.disabled ? "1" : "0"
|
|
13059
|
+
};
|
|
13060
|
+
if (it.name) {
|
|
13061
|
+
a_attrs["aria-label"] = it.name;
|
|
13062
|
+
a_attrs["data-i18n-aria-label"] = it.name;
|
|
13063
|
+
}
|
|
13064
|
+
lis.push([
|
|
13065
|
+
"li",
|
|
13066
|
+
{},
|
|
13067
|
+
[[
|
|
13068
|
+
"a",
|
|
13069
|
+
a_attrs,
|
|
13070
|
+
children
|
|
13071
|
+
]]
|
|
13072
|
+
]);
|
|
13073
|
+
}
|
|
13074
|
+
return createElement2([
|
|
13075
|
+
"nav",
|
|
13076
|
+
{
|
|
13077
|
+
class: "breadcrumb is-small",
|
|
13078
|
+
"aria-label": "breadcrumbs"
|
|
13079
|
+
},
|
|
13080
|
+
[[
|
|
13081
|
+
"ul",
|
|
13082
|
+
{},
|
|
13083
|
+
lis
|
|
13084
|
+
]]
|
|
13085
|
+
]);
|
|
13086
|
+
}
|
|
12836
13087
|
function render_backbar(gobj) {
|
|
12837
13088
|
let back_route = gobj_read_attr(gobj, "back_route") || "";
|
|
12838
13089
|
let label = gobj_read_attr(gobj, "nav_label") || "";
|
|
@@ -13238,6 +13489,7 @@ function register_c_yui_nav() {
|
|
|
13238
13489
|
//#endregion
|
|
13239
13490
|
//#region src/shell_route_map.js
|
|
13240
13491
|
var WIN_NAME$1 = "shell-route-map-window";
|
|
13492
|
+
var __show_refs__ = true;
|
|
13241
13493
|
var __open_modal__ = null;
|
|
13242
13494
|
function scroll_to_current($body) {
|
|
13243
13495
|
let $cur = $body.querySelector(".ROUTEMAP_CURRENT");
|
|
@@ -13283,6 +13535,16 @@ function render_node(node) {
|
|
|
13283
13535
|
{ class: "ROUTEMAP_EVENT" },
|
|
13284
13536
|
node.event
|
|
13285
13537
|
]);
|
|
13538
|
+
if (node.ref) row.push([
|
|
13539
|
+
"span",
|
|
13540
|
+
{
|
|
13541
|
+
class: "ROUTEMAP_REF",
|
|
13542
|
+
i18n: "shown above",
|
|
13543
|
+
title: "shown above",
|
|
13544
|
+
"data-i18n-title": "shown above"
|
|
13545
|
+
},
|
|
13546
|
+
"shown above"
|
|
13547
|
+
]);
|
|
13286
13548
|
if (node.current) row.push([
|
|
13287
13549
|
"span",
|
|
13288
13550
|
{
|
|
@@ -13291,7 +13553,7 @@ function render_node(node) {
|
|
|
13291
13553
|
},
|
|
13292
13554
|
"you are here"
|
|
13293
13555
|
]);
|
|
13294
|
-
let row_class = node.current ? " ROUTEMAP_CURRENT" : "";
|
|
13556
|
+
let row_class = (node.current ? " ROUTEMAP_CURRENT" : "") + (node.ref ? " ROUTEMAP_IS_REF" : "");
|
|
13295
13557
|
let $row;
|
|
13296
13558
|
if (node.route) $row = createElement2([
|
|
13297
13559
|
"a",
|
|
@@ -13324,21 +13586,37 @@ function render_node(node) {
|
|
|
13324
13586
|
* descendant matches (keep the ancestor path). Empty `q` shows all.
|
|
13325
13587
|
* Matches against the visible row text (name + route + event), so it
|
|
13326
13588
|
* honours the current translation. Returns whether the <li> is shown.
|
|
13589
|
+
*
|
|
13590
|
+
* `show_refs` off hides the reference rows — the second and third
|
|
13591
|
+
* place a route is reachable from, which carry no structure. They are
|
|
13592
|
+
* hidden by DEFAULT: in an app with a drawer and an account menu they
|
|
13593
|
+
* are a third of the map, and what a reader comes here for is the
|
|
13594
|
+
* structure. A ref stays hidden even inside a matched subtree — an
|
|
13595
|
+
* ancestor match must not smuggle it back in — and a group left with
|
|
13596
|
+
* nothing but refs collapses with them, since a group is only ever
|
|
13597
|
+
* shown by what it contains.
|
|
13327
13598
|
***************************************************************/
|
|
13328
|
-
function filter_li($li, q, ancestor_match) {
|
|
13599
|
+
function filter_li($li, q, ancestor_match, show_refs) {
|
|
13329
13600
|
let $row = $li.querySelector(":scope > .ROUTEMAP_ROW");
|
|
13330
13601
|
let text = $row ? ($row.textContent || "").toLowerCase() : "";
|
|
13602
|
+
let hidden_ref = !show_refs && !!($row && $row.classList.contains("ROUTEMAP_IS_REF"));
|
|
13331
13603
|
let self_match = q === "" || text.indexOf(q) >= 0;
|
|
13332
|
-
let show_all = ancestor_match || self_match;
|
|
13604
|
+
let show_all = (ancestor_match || self_match) && !hidden_ref;
|
|
13333
13605
|
let any_child = false;
|
|
13334
13606
|
let $ul = $li.querySelector(":scope > ul");
|
|
13335
13607
|
if ($ul) {
|
|
13336
13608
|
let kids = $ul.children;
|
|
13337
|
-
for (let i = 0; i < kids.length; i++) if (filter_li(kids[i], q, show_all)) any_child = true;
|
|
13609
|
+
for (let i = 0; i < kids.length; i++) if (filter_li(kids[i], q, show_all, show_refs)) any_child = true;
|
|
13338
13610
|
}
|
|
13339
|
-
let
|
|
13611
|
+
let is_group = !!($row && $row.classList.contains("ROUTEMAP_STRUCT"));
|
|
13612
|
+
let had_children = !!($ul && $ul.children.length);
|
|
13613
|
+
if (is_group && had_children && !any_child && !show_refs) {
|
|
13614
|
+
let kids = $ul.children;
|
|
13615
|
+
for (let i = 0; i < kids.length; i++) if (filter_li(kids[i], q, show_all, true)) any_child = true;
|
|
13616
|
+
}
|
|
13617
|
+
let visible = is_group && had_children ? any_child || q !== "" && self_match : (show_all || any_child) && !hidden_ref;
|
|
13340
13618
|
$li.style.display = visible ? "" : "none";
|
|
13341
|
-
if ($row) $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match);
|
|
13619
|
+
if ($row) $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match && visible);
|
|
13342
13620
|
return visible;
|
|
13343
13621
|
}
|
|
13344
13622
|
/***************************************************************
|
|
@@ -13348,16 +13626,17 @@ function filter_li($li, q, ancestor_match) {
|
|
|
13348
13626
|
* on a resting-route change. `on_jump()` closes the host for the
|
|
13349
13627
|
* one click that navigates nowhere (the current route).
|
|
13350
13628
|
***************************************************************/
|
|
13351
|
-
function build_body(shell, t
|
|
13629
|
+
function build_body(shell, t) {
|
|
13352
13630
|
let map = yui_shell_nav_map(shell);
|
|
13353
|
-
let children = [
|
|
13631
|
+
let children = [].concat(map.nav);
|
|
13632
|
+
if (Array.isArray(map.toolbar) && map.toolbar.length) children.push({
|
|
13354
13633
|
label: "toolbar",
|
|
13355
13634
|
icon: "",
|
|
13356
13635
|
route: "",
|
|
13357
13636
|
event: "",
|
|
13358
13637
|
kind: "group",
|
|
13359
13638
|
children: map.toolbar
|
|
13360
|
-
}
|
|
13639
|
+
});
|
|
13361
13640
|
if (Array.isArray(map.other) && map.other.length) children.push({
|
|
13362
13641
|
label: "other routes",
|
|
13363
13642
|
icon: "",
|
|
@@ -13410,21 +13689,53 @@ function build_body(shell, t, on_jump) {
|
|
|
13410
13689
|
"matches"
|
|
13411
13690
|
]]
|
|
13412
13691
|
]);
|
|
13692
|
+
let $refs_input = createElement2(["input", {
|
|
13693
|
+
class: "ROUTEMAP_REFS_INPUT",
|
|
13694
|
+
type: "checkbox"
|
|
13695
|
+
}]);
|
|
13696
|
+
if (__show_refs__) $refs_input.checked = true;
|
|
13697
|
+
let $refs_toggle = createElement2([
|
|
13698
|
+
"label",
|
|
13699
|
+
{
|
|
13700
|
+
class: "checkbox is-size-7 ROUTEMAP_REFS_TOGGLE",
|
|
13701
|
+
title: "show references",
|
|
13702
|
+
"data-i18n-title": "show references"
|
|
13703
|
+
},
|
|
13704
|
+
[$refs_input, [
|
|
13705
|
+
"span",
|
|
13706
|
+
{
|
|
13707
|
+
class: "ml-1",
|
|
13708
|
+
i18n: "show references"
|
|
13709
|
+
},
|
|
13710
|
+
"show references"
|
|
13711
|
+
]]
|
|
13712
|
+
]);
|
|
13413
13713
|
let $search_row = createElement2([
|
|
13414
13714
|
"div",
|
|
13415
13715
|
{ class: "ROUTEMAP_SEARCH_ROW mb-2" },
|
|
13416
|
-
[
|
|
13716
|
+
[
|
|
13717
|
+
$search_ctrl,
|
|
13718
|
+
$count,
|
|
13719
|
+
$refs_toggle
|
|
13720
|
+
]
|
|
13417
13721
|
]);
|
|
13418
|
-
|
|
13722
|
+
let apply_filter = function() {
|
|
13419
13723
|
let q = ($search.value || "").trim().toLowerCase();
|
|
13420
13724
|
let $root_li = $tree.querySelector(".ROUTEMAP_ROOT > li");
|
|
13421
|
-
if ($root_li) filter_li($root_li, q, false);
|
|
13725
|
+
if ($root_li) filter_li($root_li, q, false, __show_refs__);
|
|
13422
13726
|
if (q === "") $count.classList.add("is-hidden");
|
|
13423
13727
|
else {
|
|
13424
13728
|
$count_n.textContent = $tree.querySelectorAll(".ROUTEMAP_MATCH").length;
|
|
13425
13729
|
$count.classList.remove("is-hidden");
|
|
13426
13730
|
}
|
|
13731
|
+
};
|
|
13732
|
+
$search.addEventListener("input", apply_filter);
|
|
13733
|
+
$refs_input.addEventListener("change", function() {
|
|
13734
|
+
__show_refs__ = !!$refs_input.checked;
|
|
13735
|
+
apply_filter();
|
|
13427
13736
|
});
|
|
13737
|
+
if ($tree.querySelectorAll(".ROUTEMAP_IS_REF").length === 0) $refs_toggle.classList.add("is-hidden");
|
|
13738
|
+
apply_filter();
|
|
13428
13739
|
let $body = createElement2([
|
|
13429
13740
|
"div",
|
|
13430
13741
|
{ class: "C_YUI_SHELL_ROUTEMAP ROUTEMAP_BODY" },
|
|
@@ -13467,19 +13778,55 @@ function build_body(shell, t, on_jump) {
|
|
|
13467
13778
|
if ($area.parentNode) $area.parentNode.removeChild($area);
|
|
13468
13779
|
}
|
|
13469
13780
|
});
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
if (href && typeof window !== "undefined" && window.location.hash === href && typeof on_jump === "function") {
|
|
13475
|
-
ev.preventDefault();
|
|
13476
|
-
on_jump();
|
|
13781
|
+
let on_hash = function() {
|
|
13782
|
+
if (typeof document === "undefined" || !$body.isConnected) {
|
|
13783
|
+
if (typeof window !== "undefined") window.removeEventListener("hashchange", on_hash);
|
|
13784
|
+
return;
|
|
13477
13785
|
}
|
|
13478
|
-
|
|
13786
|
+
mark_current_row($body, window.location.hash, t);
|
|
13787
|
+
};
|
|
13788
|
+
if (typeof window !== "undefined") window.addEventListener("hashchange", on_hash);
|
|
13479
13789
|
refresh_language($body, t);
|
|
13480
13790
|
return $body;
|
|
13481
13791
|
}
|
|
13482
13792
|
/***************************************************************
|
|
13793
|
+
* Move the "you are here" mark to the row that best matches
|
|
13794
|
+
* `hash`: an exact route wins, else the LONGEST declared route
|
|
13795
|
+
* that is a path-prefix of it (the base view of a deep subpath
|
|
13796
|
+
* position). Same rule as the model's mark_current(), applied
|
|
13797
|
+
* to the rendered rows so the open panel stays truthful.
|
|
13798
|
+
***************************************************************/
|
|
13799
|
+
function mark_current_row($body, hash, t) {
|
|
13800
|
+
let route = String(hash || "").replace(/^#/, "");
|
|
13801
|
+
let $best = null;
|
|
13802
|
+
let best_len = -1;
|
|
13803
|
+
for (let $a of $body.querySelectorAll(".ROUTEMAP_LINK")) {
|
|
13804
|
+
let r = String($a.getAttribute("href") || "").replace(/^#/, "");
|
|
13805
|
+
if (!r) continue;
|
|
13806
|
+
if (r === route) {
|
|
13807
|
+
$best = $a;
|
|
13808
|
+
break;
|
|
13809
|
+
}
|
|
13810
|
+
if (route.indexOf(r + "/") === 0 && r.length > best_len) {
|
|
13811
|
+
$best = $a;
|
|
13812
|
+
best_len = r.length;
|
|
13813
|
+
}
|
|
13814
|
+
}
|
|
13815
|
+
for (let $marked of $body.querySelectorAll(".ROUTEMAP_CURRENT")) $marked.classList.remove("ROUTEMAP_CURRENT");
|
|
13816
|
+
if (!$best) return;
|
|
13817
|
+
$best.classList.add("ROUTEMAP_CURRENT");
|
|
13818
|
+
let $badge = $body.querySelector(".ROUTEMAP_HERE");
|
|
13819
|
+
if (!$badge) $badge = createElement2([
|
|
13820
|
+
"span",
|
|
13821
|
+
{
|
|
13822
|
+
class: "ROUTEMAP_HERE",
|
|
13823
|
+
i18n: "you are here"
|
|
13824
|
+
},
|
|
13825
|
+
t ? t("you are here") : "you are here"
|
|
13826
|
+
]);
|
|
13827
|
+
$best.appendChild($badge);
|
|
13828
|
+
}
|
|
13829
|
+
/***************************************************************
|
|
13483
13830
|
* Show the site map. Toggles: a second call closes the open one.
|
|
13484
13831
|
***************************************************************/
|
|
13485
13832
|
function yui_shell_show_route_map(shell, opts) {
|
|
@@ -13497,9 +13844,7 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
13497
13844
|
}
|
|
13498
13845
|
if (gclass_find_by_name("C_YUI_WINDOW") !== null) {
|
|
13499
13846
|
let win_ref = { gobj: null };
|
|
13500
|
-
let $body = build_body(shell, t
|
|
13501
|
-
if (win_ref.gobj && is_gobj(win_ref.gobj)) gobj_send_event(win_ref.gobj, "EV_CLOSE_WINDOW", {}, shell);
|
|
13502
|
-
});
|
|
13847
|
+
let $body = build_body(shell, t);
|
|
13503
13848
|
let win = gobj_create_service(WIN_NAME$1, "C_YUI_WINDOW", {
|
|
13504
13849
|
$parent: yui_shell_popup_layer(shell) || typeof document !== "undefined" && document.getElementById("top-layer") || null,
|
|
13505
13850
|
subscriber: null,
|
|
@@ -13515,7 +13860,8 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
13515
13860
|
title: "site map",
|
|
13516
13861
|
icon: "yi-bars",
|
|
13517
13862
|
body: $body,
|
|
13518
|
-
manager:
|
|
13863
|
+
manager: gobj_find_service("__window_manager__", false),
|
|
13864
|
+
keep_on_navigate: true
|
|
13519
13865
|
}, shell);
|
|
13520
13866
|
if (!win) {
|
|
13521
13867
|
log_error("C_YUI_SHELL: cannot create the site-map window");
|
|
@@ -13527,9 +13873,7 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
13527
13873
|
return win;
|
|
13528
13874
|
}
|
|
13529
13875
|
let modal_ref = { modal: null };
|
|
13530
|
-
let $body = build_body(shell, t
|
|
13531
|
-
if (modal_ref.modal && typeof modal_ref.modal.close === "function") modal_ref.modal.close();
|
|
13532
|
-
});
|
|
13876
|
+
let $body = build_body(shell, t);
|
|
13533
13877
|
modal_ref.modal = yui_shell_show_modal(shell, $body, {
|
|
13534
13878
|
dialog: true,
|
|
13535
13879
|
logical_class: "ROUTEMAP_SHEET",
|
|
@@ -51997,7 +52341,7 @@ function render_full(e) {
|
|
|
51997
52341
|
let payload = full_sections(e.kw ? e.kw : e.jn);
|
|
51998
52342
|
let text;
|
|
51999
52343
|
try {
|
|
52000
|
-
text = JSON.stringify(payload, null,
|
|
52344
|
+
text = JSON.stringify(payload, null, 4);
|
|
52001
52345
|
} catch (err) {
|
|
52002
52346
|
text = String(payload);
|
|
52003
52347
|
}
|
|
@@ -52260,7 +52604,7 @@ function info_log(level, msg, hora) {
|
|
|
52260
52604
|
let text;
|
|
52261
52605
|
if (lvl === "json") {
|
|
52262
52606
|
try {
|
|
52263
|
-
text = JSON.stringify(msg, null,
|
|
52607
|
+
text = JSON.stringify(msg, null, 4);
|
|
52264
52608
|
} catch (e) {
|
|
52265
52609
|
text = String(msg);
|
|
52266
52610
|
}
|
|
@@ -52434,7 +52778,7 @@ function traffic_to_text() {
|
|
|
52434
52778
|
out.push(head);
|
|
52435
52779
|
let payload = e.kw ? e.kw : e.jn;
|
|
52436
52780
|
try {
|
|
52437
|
-
out.push(JSON.stringify(payload, null,
|
|
52781
|
+
out.push(JSON.stringify(payload, null, 4));
|
|
52438
52782
|
} catch (err) {
|
|
52439
52783
|
out.push(String(payload));
|
|
52440
52784
|
}
|