@yuneta/gobj-ui 7.26.29 → 7.26.30

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.
@@ -65,6 +65,7 @@ import {
65
65
 
66
66
  import { resolve_route, normalize_route } from "./route_resolver.js";
67
67
  import { text_node } from "./yui_text.js";
68
+ import { badge_node, badge_tone, paint_badge } from "./yui_badge.js";
68
69
  import { build_nav_map } from "./route_map_model.js";
69
70
  import {
70
71
  section_index_target,
@@ -118,6 +119,14 @@ let PRIVATE_DATA = {
118
119
  stages: {},
119
120
  navs: [],
120
121
  item_index: {},
122
+ /* Menu item id -> {badge, badge_tone}: what yui_shell_set_nav_item_badge()
123
+ * said last for that id. It wins over the item's own `badge`, and
124
+ * every nav the shell creates is handed it (`badges`), so a nav built
125
+ * or rebuilt after the call draws it too. */
126
+ nav_badges: {},
127
+ /* Parent item id -> the items yui_shell_set_submenu() put in place:
128
+ * menu items as much as the declared ones (nav_item_declared). */
129
+ dynamic_items: {},
121
130
  /* Section route -> last position visited inside it. Mirrors the
122
131
  * url for the duration of the page; see remember_position(). */
123
132
  section_pos: {},
@@ -1243,6 +1252,7 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
1243
1252
  level: level,
1244
1253
  show_on: render_cfg.show_on || "",
1245
1254
  back_route: render_cfg.back_route || "",
1255
+ badges: Object.assign({}, priv.nav_badges),
1246
1256
  shell: gobj
1247
1257
  },
1248
1258
  gobj
@@ -1694,6 +1704,7 @@ function navigate_to(gobj, route, depth, no_drain)
1694
1704
 
1695
1705
  function build_view_gobj(gobj, entry, route, stage)
1696
1706
  {
1707
+ let priv = gobj.priv;
1697
1708
  let target = entry.target;
1698
1709
  /* The deep-link tail is NOT injected into kw: gobj_create2
1699
1710
  * validates kw against the view's SDATA strictly, so an extra
@@ -1702,6 +1713,11 @@ function build_view_gobj(gobj, entry, route, stage)
1702
1713
  * mount (and on every later subpath-only change) — that single
1703
1714
  * mechanism is how a view owns its dynamic 3rd level. */
1704
1715
  let kw = target.kw || {};
1716
+ if(target.gclass === "C_YUI_NAV") {
1717
+ /* A section-index landing is a nav of the shell, mounted when
1718
+ * the user first walks into it: it draws the badges set before. */
1719
+ kw = Object.assign({}, kw, {badges: Object.assign({}, priv.nav_badges)});
1720
+ }
1705
1721
  let name = target.name || `view_${safe_id(route)}`;
1706
1722
 
1707
1723
  let view;
@@ -1848,51 +1864,6 @@ function build_toolbar(gobj, config)
1848
1864
  $zone.appendChild($bar);
1849
1865
  }
1850
1866
 
1851
- /************************************************************
1852
- * The badge node of a toolbar item — the little count pinned to
1853
- * the corner of its icon (unread notifications, active alarms).
1854
- *
1855
- * ALWAYS rendered, hidden when empty, so the runtime setter has a
1856
- * node to write into and does not have to rebuild the button.
1857
- *
1858
- * EMPTY IS 0, "" AND null ALIKE: a badge reading "0" is worse than
1859
- * no badge — it draws the eye to say nothing. A count is only
1860
- * worth a pixel when there IS something.
1861
- *
1862
- * Capped at 99+: the toolbar is a fixed-width row, and a four
1863
- * digit pill pushes the items next to it off a phone screen.
1864
- *
1865
- * role="status" and NOT aria-hidden, deliberately. The button
1866
- * carries an explicit aria-label, and an explicit label REPLACES
1867
- * the element's content for a screen reader — so a badge inside
1868
- * it would be silent. As its own live region it is both read and
1869
- * announced when the count changes, which is the whole point of a
1870
- * badge that means "something needs you".
1871
- ************************************************************/
1872
- function badge_text(value)
1873
- {
1874
- if(value === null || value === undefined || value === false) {
1875
- return "";
1876
- }
1877
- if(typeof value === "number") {
1878
- if(!isFinite(value) || value <= 0) {
1879
- return "";
1880
- }
1881
- return (value > 99) ? "99+" : String(Math.floor(value));
1882
- }
1883
- return String(value).trim();
1884
- }
1885
-
1886
- function badge_node(value)
1887
- {
1888
- let text = badge_text(value);
1889
- let attrs = {class: "yui-toolbar-badge", role: "status"};
1890
- if(!text) {
1891
- attrs.hidden = "hidden";
1892
- }
1893
- return ["span", attrs, text];
1894
- }
1895
-
1896
1867
  /************************************************************
1897
1868
  * The attributes that turn a toolbar item into a real link, or
1898
1869
  * null when this item is not one. Answering null is what keeps
@@ -1931,11 +1902,14 @@ function build_toolbar_action_item(gobj, it)
1931
1902
  if(!empty_string(it.icon)) {
1932
1903
  /* The badge is anchored to the ICON, not to the button: with a
1933
1904
  * label on (desktop) the button is wide, and a badge pinned to
1934
- * its corner would float far from the glyph it counts. */
1905
+ * its corner would float far from the glyph it counts. The
1906
+ * count, its tone and their rules: yui_badge.js. */
1935
1907
  children.push(["span", {class: "icon yui-toolbar-icon"},
1936
1908
  [
1937
1909
  ["i", {class: it.icon, "aria-hidden": "true"}],
1938
- badge_node(it.badge)
1910
+ badge_node(it.badge, it.badge_tone,
1911
+ "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge",
1912
+ `C_YUI_SHELL: toolbar item '${it.id || "?"}'`)
1939
1913
  ]
1940
1914
  ]);
1941
1915
  }
@@ -3220,6 +3194,60 @@ function update_secondary_nav_visibility(gobj, entry)
3220
3194
  }
3221
3195
  }
3222
3196
 
3197
+ /************************************************************
3198
+ * Does a menu of this shell declare an item `item_id`: an item
3199
+ * of a menu of app_config.json, of one of their submenus, or
3200
+ * one given to yui_shell_set_submenu() since.
3201
+ ************************************************************/
3202
+ function nav_item_declared(gobj, item_id)
3203
+ {
3204
+ let priv = gobj.priv;
3205
+ let config = gobj_read_attr(gobj, "config") || {};
3206
+ let menus = is_object(config.menu) ? config.menu : {};
3207
+ let holds = (items) => is_array(items) && items.some((it) => it && it.id === item_id);
3208
+
3209
+ for(let menu_id in menus) {
3210
+ let items = menus[menu_id] && menus[menu_id].items;
3211
+ if(!is_array(items)) {
3212
+ continue;
3213
+ }
3214
+ if(holds(items)) {
3215
+ return true;
3216
+ }
3217
+ for(let it of items) {
3218
+ if(it && it.submenu && holds(it.submenu.items)) {
3219
+ return true;
3220
+ }
3221
+ }
3222
+ }
3223
+ for(let parent_item_id in priv.dynamic_items) {
3224
+ if(holds(priv.dynamic_items[parent_item_id])) {
3225
+ return true;
3226
+ }
3227
+ }
3228
+ return false;
3229
+ }
3230
+
3231
+ /************************************************************
3232
+ * Every C_YUI_NAV of this shell: the navs of its menus, and the
3233
+ * section-index landings mounted in a stage.
3234
+ ************************************************************/
3235
+ function shell_navs(gobj)
3236
+ {
3237
+ let priv = gobj.priv;
3238
+ let navs = priv.navs.slice();
3239
+ for(let stage_name in priv.stages) {
3240
+ let views = priv.stages[stage_name].items || {};
3241
+ for(let route in views) {
3242
+ let view = views[route];
3243
+ if(view && gobj_gclass_name(view) === "C_YUI_NAV") {
3244
+ navs.push(view);
3245
+ }
3246
+ }
3247
+ }
3248
+ return navs;
3249
+ }
3250
+
3223
3251
 
3224
3252
 
3225
3253
 
@@ -3349,8 +3377,11 @@ function ac_nav_item_close(gobj, event, kw, src)
3349
3377
  * items of a primary item's submenu (its secondary nav) at runtime
3350
3378
  * and re-registers their routes so navigation resolves. Item
3351
3379
  * descriptors may carry { id, name, icon, route, class, closable,
3352
- * target:{stage,gclass,kw,lifecycle} }. Routes present before but
3353
- * absent now are pruned (index entry removed, mounted view destroyed).
3380
+ * badge, badge_tone, target:{stage,gclass,kw,lifecycle} }. Routes
3381
+ * present before but absent now are pruned (index entry removed,
3382
+ * mounted view destroyed). A badge set with
3383
+ * yui_shell_set_nav_item_badge() for an id stays on that id here too:
3384
+ * the rebuilt nav draws it, whatever the descriptor's own `badge`.
3354
3385
  ************************************************************/
3355
3386
  function find_secondary_nav(priv, parent_item_id)
3356
3387
  {
@@ -3471,6 +3502,7 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
3471
3502
  };
3472
3503
  }
3473
3504
  priv.dynamic_routes[parent_item_id] = new_routes;
3505
+ priv.dynamic_items[parent_item_id] = items;
3474
3506
 
3475
3507
  /* Push the new items into the nav (rebuilds its DOM in place), and
3476
3508
  * re-apply the translator: the rebuilt labels carry their keys as
@@ -4233,22 +4265,31 @@ function yui_shell_set_toolbar_item_icon(shell_gobj, item_id, icon_class)
4233
4265
  * its icon.
4234
4266
  *
4235
4267
  * yui_shell_set_toolbar_item_badge(shell, "alarms", 3);
4268
+ * yui_shell_set_toolbar_item_badge(shell, "alarms", 3, "warning");
4236
4269
  * yui_shell_set_toolbar_item_badge(shell, "alarms", 0); // hidden
4237
4270
  *
4238
4271
  * This is the API that matters: a count is a RUNTIME fact. The
4239
- * `badge` field of a toolbar item only seeds the first paint, and
4240
- * an app whose number never changes did not need a badge.
4272
+ * `badge` / `badge_tone` fields of a toolbar item only seed the
4273
+ * first paint, and an app whose number never changes did not need
4274
+ * a badge.
4241
4275
  *
4242
4276
  * 0 / "" / null / false all clear it, and a number over 99 renders
4243
- * "99+" — see badge_node(). Accepts a string for the states that
4244
- * are not counts ("!", "…").
4277
+ * "99+" — see yui_badge.js. Accepts a string for the states that
4278
+ * are not counts ("!", "…"). `tone`, optional, is one of
4279
+ * BADGE_TONES ("danger" | "warning" | "info" | "success" |
4280
+ * "primary" | "link"): Bulma's is-<tone> on the badge. Each call
4281
+ * states the WHOLE badge: no tone is the default look, not the
4282
+ * tone it had. The twin of yui_shell_set_nav_item_badge().
4245
4283
  *
4246
4284
  * Silent no-op on an unknown item id: a toolbar that does not
4247
4285
  * declare the item (an app without alarms) must not be an error
4248
- * at every tick of whatever feeds the number.
4286
+ * at every tick of whatever feeds the number. An unknown TONE is
4287
+ * the caller's error: logged, and drawn with the default look.
4249
4288
  ************************************************************/
4250
- function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value)
4289
+ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value, tone)
4251
4290
  {
4291
+ tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_toolbar_item_badge('${item_id}')`);
4292
+
4252
4293
  if(!shell_gobj || !is_gobj(shell_gobj) || empty_string(item_id)) {
4253
4294
  return;
4254
4295
  }
@@ -4262,21 +4303,59 @@ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value)
4262
4303
  if(!$badge) {
4263
4304
  return;
4264
4305
  }
4306
+ paint_badge($badge, value, tone);
4307
+ }
4265
4308
 
4266
- let text = badge_text(value);
4267
- if(!text) {
4268
- $badge.textContent = "";
4269
- $badge.setAttribute("hidden", "hidden");
4309
+ /************************************************************
4310
+ * Set (or clear) the badge of a MENU item — the twin of
4311
+ * yui_shell_set_toolbar_item_badge(), for an entry of a menu of
4312
+ * app_config.json, of one of their submenus, or one given to
4313
+ * yui_shell_set_submenu().
4314
+ *
4315
+ * yui_shell_set_nav_item_badge(shell, "alarms", 3);
4316
+ * yui_shell_set_nav_item_badge(shell, "alarms", 3, "warning");
4317
+ * yui_shell_set_nav_item_badge(shell, "alarms", 0); // hidden
4318
+ *
4319
+ * The same value, tone and rules as the toolbar's (yui_badge.js),
4320
+ * each call stating the whole badge.
4321
+ *
4322
+ * EVERY instance of the item gets it: the same entry is drawn by
4323
+ * as many navs as there are zones hosting its menu (a rail on
4324
+ * desktop, an icon bar on a phone), and by a drawer that repeats
4325
+ * it. Each nav repaints it in place (EV_SET_ITEM_BADGE), and
4326
+ * KEEPS it: a rebuild of the nav (a yui_shell_set_submenu()) draws
4327
+ * it again. The shell keeps it too, for the navs it creates
4328
+ * afterwards (a section-index landing, mounted when the user walks
4329
+ * into it). From then on it is the badge of that id: the `badge`
4330
+ * field of an item only seeds the paint until this is called.
4331
+ *
4332
+ * Silent no-op on an id no menu declares, as the toolbar's: a menu
4333
+ * without the entry (an app without alarms) must not be an error
4334
+ * at every tick of whatever feeds the number. An unknown TONE is
4335
+ * the caller's error: logged, and drawn with the default look.
4336
+ ************************************************************/
4337
+ function yui_shell_set_nav_item_badge(shell_gobj, item_id, value, tone)
4338
+ {
4339
+ tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_nav_item_badge('${item_id}')`);
4340
+
4341
+ /* Silent, as the toolbar's: whatever feeds the number may tick
4342
+ * before the shell exists, after it is gone, and in an app whose
4343
+ * menus have no such entry. */
4344
+ if(!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj) ||
4345
+ empty_string(item_id)) {
4270
4346
  return;
4271
4347
  }
4272
- /* Do not touch the DOM when nothing changed: role="status" is a
4273
- * live region, and rewriting the same number would have a screen
4274
- * reader announce it again on every tick. */
4275
- if($badge.textContent === text && !$badge.hasAttribute("hidden")) {
4348
+ let priv = shell_gobj.priv;
4349
+ if(!priv || !nav_item_declared(shell_gobj, item_id)) {
4276
4350
  return;
4277
4351
  }
4278
- $badge.textContent = text;
4279
- $badge.removeAttribute("hidden");
4352
+ let badge = (value === undefined) ? null : value;
4353
+ priv.nav_badges[item_id] = {badge: badge, badge_tone: tone};
4354
+
4355
+ for(let nav of shell_navs(shell_gobj)) {
4356
+ gobj_send_event(nav, "EV_SET_ITEM_BADGE",
4357
+ {item_id: item_id, badge: badge, badge_tone: tone}, shell_gobj);
4358
+ }
4280
4359
  }
4281
4360
 
4282
4361
  /************************************************************
@@ -4330,6 +4409,7 @@ export {
4330
4409
  yui_shell_set_connection_state,
4331
4410
  yui_shell_set_toolbar_item_icon,
4332
4411
  yui_shell_set_toolbar_item_badge,
4412
+ yui_shell_set_nav_item_badge,
4333
4413
  yui_shell_close_dropdown,
4334
4414
  yui_shell_set_submenu
4335
4415
  };
@@ -18,19 +18,34 @@ import {text_node} from "./yui_text.js";
18
18
  * contract as every other nav item (data-route/data-item-id/
19
19
  * data-disabled), so C_YUI_NAV's delegated click handler and
20
20
  * active-route highlight work unchanged.
21
+ *
22
+ * `badge_of(it, pinned)`, optional, answers the description of
23
+ * the card's badge (C_YUI_NAV's, which knows what was set at
24
+ * runtime). A card is an icon over its label, so the badge goes
25
+ * on the corner of the icon (`pinned`), or after the label of a
26
+ * card that has none. Without it a card has no badge.
21
27
  ************************************************************/
22
- export function card_descriptor(it, show_label)
28
+ export function card_descriptor(it, show_label, badge_of)
23
29
  {
24
30
  let children = [];
25
31
  let label = it.name || "";
32
+ let pinned = !!it.icon;
33
+ let badge = badge_of ? badge_of(it, pinned) : null;
26
34
 
27
35
  if(it.icon) {
28
- children.push(["span", {class: "icon is-medium"},
29
- ["i", {class: it.icon, "aria-hidden": "true"}]]);
36
+ let glyph = ["i", {class: it.icon, "aria-hidden": "true"}];
37
+ if(badge) {
38
+ children.push(["span", {class: "icon is-medium yui-nav-icon"}, [glyph, badge]]);
39
+ } else {
40
+ children.push(["span", {class: "icon is-medium"}, glyph]);
41
+ }
30
42
  }
31
43
  if(show_label && label) {
32
44
  children.push(["span", {class: "yui-nav-label", i18n: label}, text_node(label)]);
33
45
  }
46
+ if(badge && !pinned) {
47
+ children.push(badge);
48
+ }
34
49
 
35
50
  let a_attrs = {
36
51
  class: "yui-nav-item yui-nav-card",
@@ -58,15 +73,16 @@ export function card_descriptor(it, show_label)
58
73
  /************************************************************
59
74
  * Descriptor of the whole grid. Decorative items (`header`/
60
75
  * `divider`) are dropped, same policy as the tabs layout.
76
+ * `badge_of`: as card_descriptor().
61
77
  ************************************************************/
62
- export function cards_grid_descriptor(items, show_label)
78
+ export function cards_grid_descriptor(items, show_label, badge_of)
63
79
  {
64
80
  let cards = [];
65
81
  for(let it of (items || [])) {
66
82
  if(!it || it.type === "header" || it.type === "divider") {
67
83
  continue;
68
84
  }
69
- cards.push(card_descriptor(it, show_label));
85
+ cards.push(card_descriptor(it, show_label, badge_of));
70
86
  }
71
87
  return ["div", {}, cards];
72
88
  }