@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.
@@ -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 === "/") break;
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:" + (.4 + depth * 1.1) + "em;";
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, 2);
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 visible = show_all || any_child;
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, on_jump) {
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
- }].concat(map.nav);
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
- [$search_ctrl, $count]
13716
+ [
13717
+ $search_ctrl,
13718
+ $count,
13719
+ $refs_toggle
13720
+ ]
13417
13721
  ]);
13418
- $search.addEventListener("input", function() {
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
- $body.addEventListener("click", function(ev) {
13471
- let $link = ev.target && ev.target.closest && ev.target.closest(".ROUTEMAP_LINK");
13472
- if (!$link) return;
13473
- let href = $link.getAttribute("href");
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, function() {
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: null
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, function() {
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, 2);
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, 2);
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, 2));
52781
+ out.push(JSON.stringify(payload, null, 4));
52438
52782
  } catch (err) {
52439
52783
  out.push(String(payload));
52440
52784
  }