@yuneta/gobj-ui 5.4.0 → 5.6.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.
@@ -1542,10 +1542,8 @@ function build_view_gobj(gobj, entry, route, stage)
1542
1542
  * (synthesized "cards" C_YUI_NAV) is SHELL-owned DOM built after
1543
1543
  * the host's one-shot refresh_language — apply the registered
1544
1544
  * translator, same policy as lazily-built dropdown panels. */
1545
- let priv = gobj.priv;
1546
- if(priv && typeof priv.translator === "function" &&
1547
- target.gclass === "C_YUI_NAV") {
1548
- refresh_language($view, priv.translator);
1545
+ if(target.gclass === "C_YUI_NAV") {
1546
+ yui_shell_translate(gobj, $view);
1549
1547
  }
1550
1548
  return view;
1551
1549
  }
@@ -1643,6 +1641,51 @@ function build_toolbar(gobj, config)
1643
1641
  $zone.appendChild($bar);
1644
1642
  }
1645
1643
 
1644
+ /************************************************************
1645
+ * The badge node of a toolbar item — the little count pinned to
1646
+ * the corner of its icon (unread notifications, active alarms).
1647
+ *
1648
+ * ALWAYS rendered, hidden when empty, so the runtime setter has a
1649
+ * node to write into and does not have to rebuild the button.
1650
+ *
1651
+ * EMPTY IS 0, "" AND null ALIKE: a badge reading "0" is worse than
1652
+ * no badge — it draws the eye to say nothing. A count is only
1653
+ * worth a pixel when there IS something.
1654
+ *
1655
+ * Capped at 99+: the toolbar is a fixed-width row, and a four
1656
+ * digit pill pushes the items next to it off a phone screen.
1657
+ *
1658
+ * role="status" and NOT aria-hidden, deliberately. The button
1659
+ * carries an explicit aria-label, and an explicit label REPLACES
1660
+ * the element's content for a screen reader — so a badge inside
1661
+ * it would be silent. As its own live region it is both read and
1662
+ * announced when the count changes, which is the whole point of a
1663
+ * badge that means "something needs you".
1664
+ ************************************************************/
1665
+ function badge_text(value)
1666
+ {
1667
+ if(value === null || value === undefined || value === false) {
1668
+ return "";
1669
+ }
1670
+ if(typeof value === "number") {
1671
+ if(!isFinite(value) || value <= 0) {
1672
+ return "";
1673
+ }
1674
+ return (value > 99) ? "99+" : String(Math.floor(value));
1675
+ }
1676
+ return String(value).trim();
1677
+ }
1678
+
1679
+ function badge_node(value)
1680
+ {
1681
+ let text = badge_text(value);
1682
+ let attrs = {class: "yui-toolbar-badge", role: "status"};
1683
+ if(!text) {
1684
+ attrs.hidden = "hidden";
1685
+ }
1686
+ return ["span", attrs, text];
1687
+ }
1688
+
1646
1689
  /************************************************************
1647
1690
  * Renderer for the default ("action") item kind.
1648
1691
  ************************************************************/
@@ -1650,8 +1693,15 @@ function build_toolbar_action_item(gobj, it)
1650
1693
  {
1651
1694
  let children = [];
1652
1695
  if(!empty_string(it.icon)) {
1653
- children.push(["span", {class: "icon"},
1654
- ["i", {class: it.icon, "aria-hidden": "true"}]]);
1696
+ /* The badge is anchored to the ICON, not to the button: with a
1697
+ * label on (desktop) the button is wide, and a badge pinned to
1698
+ * its corner would float far from the glyph it counts. */
1699
+ children.push(["span", {class: "icon yui-toolbar-icon"},
1700
+ [
1701
+ ["i", {class: it.icon, "aria-hidden": "true"}],
1702
+ badge_node(it.badge)
1703
+ ]
1704
+ ]);
1655
1705
  }
1656
1706
  if(!empty_string(it.name)) {
1657
1707
  children.push(["span", {class: "yui-toolbar-item-label", i18n: it.name},
@@ -2050,9 +2100,7 @@ function open_toolbar_dropdown(gobj, item, action, $trigger)
2050
2100
  priv.layers.popup.appendChild($panel);
2051
2101
 
2052
2102
  /* Translate the lazily-built panel (see the note above). */
2053
- if(typeof priv.translator === "function") {
2054
- refresh_language($panel, priv.translator);
2055
- }
2103
+ yui_shell_translate(gobj, $panel);
2056
2104
 
2057
2105
  /* Click-outside (capture-phase mousedown) closes the dropdown.
2058
2106
  * Capture phase so a click on a sibling toolbar trigger lands
@@ -2857,10 +2905,7 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
2857
2905
  gobj_send_event(view, "EV_SET_ITEMS", {items: items}, shell_gobj);
2858
2906
  /* EV_SET_ITEMS rebuilt the DOM: re-apply the translator
2859
2907
  * (shell-owned DOM, same policy as build_view_gobj). */
2860
- let $view = gobj_read_attr(view, "$container");
2861
- if($view && typeof priv.translator === "function") {
2862
- refresh_language($view, priv.translator);
2863
- }
2908
+ yui_shell_translate(shell_gobj, gobj_read_attr(view, "$container"));
2864
2909
  }
2865
2910
  }
2866
2911
  }
@@ -3265,9 +3310,10 @@ function yui_shell_refresh_avatars(shell_gobj)
3265
3310
  * Register the host's i18n translator (a t-function:
3266
3311
  * key => translated string). The host still translates the
3267
3312
  * static shell tree itself via refresh_language($container, t);
3268
- * this is only so the shell can translate DOM it builds LAZILY
3269
- * and OUTSIDE $containertoday the toolbar dropdown panel.
3270
- * Optional: with no translator the panel renders raw keys
3313
+ * this is only so the shell and the library components that
3314
+ * build DOM under it can translate DOM built LAZILY, after
3315
+ * that one-shot pass and often outside $container.
3316
+ * Optional: with no translator such DOM renders raw keys
3271
3317
  * (the previous behaviour).
3272
3318
  ************************************************************/
3273
3319
  function yui_shell_set_translator(shell_gobj, t)
@@ -3279,6 +3325,39 @@ function yui_shell_set_translator(shell_gobj, t)
3279
3325
  priv.translator = (typeof t === "function") ? t : null;
3280
3326
  }
3281
3327
 
3328
+ /************************************************************
3329
+ * Apply the registered translator to a FRESHLY BUILT subtree.
3330
+ *
3331
+ * Carrying the `i18n` key on a node is not enough for it to
3332
+ * render translated: the node is born holding the raw English
3333
+ * key, and the host's refresh_language() passes walk what
3334
+ * ALREADY exists — the shell tree at start up, document.body on
3335
+ * a language switch. Anything built after that (a dropdown
3336
+ * panel, a nav a node renders when you walk into it) is reached
3337
+ * by neither, so it renders the key: lower-case English that
3338
+ * never changes language, i.e. exactly what a MISSING key looks
3339
+ * like.
3340
+ *
3341
+ * The division of labour, unchanged: LIBRARY-built DOM is
3342
+ * translated through here; APP view gclasses translate their own
3343
+ * DOM (they own a `t` — see mount_view).
3344
+ *
3345
+ * Silent no-op with no shell or no translator: an app that never
3346
+ * registered one keeps the previous behaviour instead of losing
3347
+ * its chrome.
3348
+ ************************************************************/
3349
+ function yui_shell_translate(shell_gobj, $el)
3350
+ {
3351
+ if(!$el || !shell_gobj || !is_gobj(shell_gobj)) {
3352
+ return;
3353
+ }
3354
+ let priv = shell_gobj.priv;
3355
+ if(!priv || typeof priv.translator !== "function") {
3356
+ return;
3357
+ }
3358
+ refresh_language($el, priv.translator);
3359
+ }
3360
+
3282
3361
  /************************************************************
3283
3362
  * The app switched the language: re-translate the whole document (every
3284
3363
  * node carrying data-i18n / data-i18n-title / data-i18n-aria-label) and
@@ -3341,6 +3420,57 @@ function yui_shell_set_toolbar_item_icon(shell_gobj, item_id, icon_class)
3341
3420
  }
3342
3421
  }
3343
3422
 
3423
+ /************************************************************
3424
+ * Set (or clear) a toolbar item's badge — the count pinned to
3425
+ * its icon.
3426
+ *
3427
+ * yui_shell_set_toolbar_item_badge(shell, "alarms", 3);
3428
+ * yui_shell_set_toolbar_item_badge(shell, "alarms", 0); // hidden
3429
+ *
3430
+ * This is the API that matters: a count is a RUNTIME fact. The
3431
+ * `badge` field of a toolbar item only seeds the first paint, and
3432
+ * an app whose number never changes did not need a badge.
3433
+ *
3434
+ * 0 / "" / null / false all clear it, and a number over 99 renders
3435
+ * "99+" — see badge_node(). Accepts a string for the states that
3436
+ * are not counts ("!", "…").
3437
+ *
3438
+ * Silent no-op on an unknown item id: a toolbar that does not
3439
+ * declare the item (an app without alarms) must not be an error
3440
+ * at every tick of whatever feeds the number.
3441
+ ************************************************************/
3442
+ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value)
3443
+ {
3444
+ if(!shell_gobj || !is_gobj(shell_gobj) || empty_string(item_id)) {
3445
+ return;
3446
+ }
3447
+ let $container = gobj_read_attr(shell_gobj, "$container");
3448
+ if(!$container) {
3449
+ return;
3450
+ }
3451
+ let $badge = $container.querySelector(
3452
+ `[data-toolbar-item-id="${item_id}"] .yui-toolbar-badge`
3453
+ );
3454
+ if(!$badge) {
3455
+ return;
3456
+ }
3457
+
3458
+ let text = badge_text(value);
3459
+ if(!text) {
3460
+ $badge.textContent = "";
3461
+ $badge.setAttribute("hidden", "hidden");
3462
+ return;
3463
+ }
3464
+ /* Do not touch the DOM when nothing changed: role="status" is a
3465
+ * live region, and rewriting the same number would have a screen
3466
+ * reader announce it again on every tick. */
3467
+ if($badge.textContent === text && !$badge.hasAttribute("hidden")) {
3468
+ return;
3469
+ }
3470
+ $badge.textContent = text;
3471
+ $badge.removeAttribute("hidden");
3472
+ }
3473
+
3344
3474
  /************************************************************
3345
3475
  * Programmatic close of any open toolbar dropdown. Useful for
3346
3476
  * external triggers (e.g. EV_LOGOUT firing from elsewhere) that
@@ -3383,9 +3513,11 @@ export {
3383
3513
  yui_shell_set_avatar_provider,
3384
3514
  yui_shell_refresh_avatars,
3385
3515
  yui_shell_set_translator,
3516
+ yui_shell_translate,
3386
3517
  yui_shell_language_changed,
3387
3518
  yui_shell_set_connection_state,
3388
3519
  yui_shell_set_toolbar_item_icon,
3520
+ yui_shell_set_toolbar_item_badge,
3389
3521
  yui_shell_close_dropdown,
3390
3522
  yui_shell_set_submenu
3391
3523
  };