@yuneta/gobj-ui 7.26.29 → 7.26.31

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/index.js CHANGED
@@ -54,6 +54,7 @@ export {
54
54
  yui_shell_set_connection_state,
55
55
  yui_shell_set_toolbar_item_icon,
56
56
  yui_shell_set_toolbar_item_badge,
57
+ yui_shell_set_nav_item_badge,
57
58
  yui_shell_close_dropdown,
58
59
  yui_shell_register_event_handler,
59
60
  yui_shell_set_sub_routes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.26.29",
3
+ "version": "7.26.31",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_form.js CHANGED
@@ -688,6 +688,12 @@ function name_form_control($field, conf)
688
688
  $control.setAttribute("aria-label", t(key));
689
689
  $control.setAttribute("data-i18n-aria-label", key);
690
690
  }
691
+ /* And its title, as every control carries: the label sits apart
692
+ * from the box, and a narrow form takes it to another line. */
693
+ if($control && !$control.getAttribute("title")) {
694
+ $control.setAttribute("title", t(key));
695
+ $control.setAttribute("data-i18n-title", key);
696
+ }
691
697
 
692
698
  /* Tom Select HIDES the <select> it is given (`ts-hidden-accessible`)
693
699
  * and draws its own text box in front of it. Naming the original
@@ -700,6 +706,10 @@ function name_form_control($field, conf)
700
706
  $ts.setAttribute("aria-label", t(key));
701
707
  $ts.setAttribute("data-i18n-aria-label", key);
702
708
  }
709
+ if($ts && !$ts.getAttribute("title")) {
710
+ $ts.setAttribute("title", t(key));
711
+ $ts.setAttribute("data-i18n-title", key);
712
+ }
703
713
 
704
714
  /* The ✕ of the clear_button plugin is drawn by Tom Select too, with a
705
715
  * title written once with t() and no key: named here, after the
@@ -2052,6 +2062,8 @@ function create_coordinates(gobj, attrs)
2052
2062
  * froze in the language it was built in. */
2053
2063
  placeholder: t('coordinates...'),
2054
2064
  'data-i18n-placeholder': 'coordinates...',
2065
+ title: t('coordinates'),
2066
+ 'data-i18n-title': 'coordinates',
2055
2067
  'aria-label': t('coordinates'),
2056
2068
  'data-i18n-aria-label': 'coordinates'},
2057
2069
  '',
package/src/c_yui_nav.js CHANGED
@@ -33,11 +33,29 @@
33
33
  *
34
34
  * Each item supports:
35
35
  * id, name, icon (CSS class or svg id), route, disabled
36
- * (`badge` was listed here for a long time and NOTHING ever
37
- * rendered it — a menu item cannot carry a count today. The
38
- * toolbar can: `badge` on a toolbar item plus
39
- * yui_shell_set_toolbar_item_badge(). Implement it here the
40
- * day a menu entry needs one, rather than re-listing it.)
36
+ * badge — a count, or a short text, carried by the item:
37
+ * the toolbar badge's rules, helper and live
38
+ * region (yui_badge.js) -- 0 / "" / null show
39
+ * none, over 99 reads "99+". It sits on the
40
+ * corner of the icon where the item is an icon
41
+ * over its label (icon-bar, cards, a stacked
42
+ * item) or an icon alone, and after the label
43
+ * where the item reads as a line (vertical,
44
+ * drawer, submenu, tabs, an accordion head).
45
+ * The field only seeds the paint: a count is a
46
+ * RUNTIME fact, set by
47
+ * yui_shell_set_nav_item_badge(shell, id, value,
48
+ * tone), which reaches every nav drawing the id.
49
+ * Not drawn by "breadcrumb", deliberately: a
50
+ * crumb names a step of the way in, and the
51
+ * count belongs to the menu entry that leads
52
+ * there -- a trail with counts on it reads as a
53
+ * list of destinations, which is what a
54
+ * breadcrumb is not. ("backbar" has no items.)
55
+ * badge_tone — "danger" (the default look), "warning",
56
+ * "info", "success", "primary" or "link":
57
+ * Bulma's is-<tone> on the badge. An unknown
58
+ * one is logged and drawn with the default look.
41
59
  * class — extra CSS class(es) on the item (tabs layout), e.g.
42
60
  * a per-item state colour like "yui-nav-disconnected"
43
61
  * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
@@ -48,6 +66,13 @@
48
66
  * active highlight restored. The app_config path is simply the
49
67
  * first, startup caller of the same mechanism.
50
68
  *
69
+ * One item's badge is set at runtime by sending EV_SET_ITEM_BADGE
70
+ * {item_id, badge, badge_tone}: painted in place, no rebuild, and
71
+ * KEPT in the `badges` attr, which wins over the item's own fields
72
+ * in every later rebuild. The shell sends it to all its navs
73
+ * (yui_shell_set_nav_item_badge) and hands `badges` to every nav
74
+ * it creates.
75
+ *
51
76
  * In addition to navigable items, secondary navs accept two
52
77
  * decorative item kinds for in-place visual grouping:
53
78
  *
@@ -71,6 +96,7 @@ import {
71
96
  gobj_parent,
72
97
  gobj_read_attr, gobj_read_pointer_attr, gobj_write_attr,
73
98
  gobj_publish_event,
99
+ gobj_short_name,
74
100
  createElement2, empty_string, is_array, is_object, is_string,
75
101
  } from "@yuneta/gobj-js";
76
102
 
@@ -83,6 +109,7 @@ import {
83
109
  bulma_hidden_class,
84
110
  } from "./shell_show_on.js";
85
111
  import {text_node} from "./yui_text.js";
112
+ import {badge_node, badge_tone, paint_badge} from "./yui_badge.js";
86
113
 
87
114
  /***************************************************************
88
115
  * Constants
@@ -118,6 +145,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "show_label", 0, true, "Show item la
118
145
  SDATA(data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
119
146
  SDATA(data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
120
147
  SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
148
+ SDATA(data_type_t.DTP_DICT, "badges", 0, "{}", "Item id -> {badge, badge_tone} set at runtime (EV_SET_ITEM_BADGE); wins over the item's own fields"),
121
149
 
122
150
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
123
151
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
@@ -409,13 +437,20 @@ function render_tabs(gobj, items)
409
437
  continue; /* tab strips have no room for section labels */
410
438
  }
411
439
  let children = [];
440
+ let has_label = show_label && !empty_string(it.name);
441
+ /* An icon-only tab carries its badge on the icon; a labelled
442
+ * one after the label, before the ✕. */
443
+ let pinned = !empty_string(it.icon) && !has_label;
444
+ let badge_el = item_badge_node(gobj, it, pinned);
412
445
  if(!empty_string(it.icon)) {
413
- children.push(["span", {class: "icon is-small"},
414
- ["i", {class: it.icon, "aria-hidden":"true"}]]);
446
+ children.push(icon_span(it.icon, "icon is-small", pinned ? badge_el : null));
415
447
  }
416
- if(show_label && !empty_string(it.name)) {
448
+ if(has_label) {
417
449
  children.push(["span", {i18n: it.name}, text_node(it.name)]);
418
450
  }
451
+ if(!pinned) {
452
+ children.push(badge_el);
453
+ }
419
454
  /* Optional close affordance: a trailing ✕ that emits
420
455
  * EV_NAV_ITEM_CLOSE instead of navigating (caught first in
421
456
  * wire_clicks by its data-close-item marker). */
@@ -462,7 +497,8 @@ function render_tabs(gobj, items)
462
497
  function render_cards(gobj, items)
463
498
  {
464
499
  let show_label = gobj_read_attr(gobj, "show_label");
465
- return createElement2(cards_grid_descriptor(items, show_label));
500
+ return createElement2(cards_grid_descriptor(items, show_label,
501
+ (it, pinned) => item_badge_node(gobj, it, pinned)));
466
502
  }
467
503
 
468
504
  /************************************************************
@@ -481,6 +517,9 @@ function render_cards(gobj, items)
481
517
  * other layout — so the delegated click handler and the active
482
518
  * highlight work unchanged, and the last crumb marks itself through
483
519
  * `active_route` like a tab does.
520
+ *
521
+ * No badge, deliberately: a crumb names a step of the way in, and a
522
+ * count belongs to the menu entry that leads there.
484
523
  ************************************************************/
485
524
  function render_breadcrumb(gobj, items)
486
525
  {
@@ -644,7 +683,9 @@ function render_accordion(gobj, items)
644
683
  head_attrs["aria-label"] = head_text;
645
684
  head_attrs["data-i18n-aria-label"] = head_text;
646
685
  }
647
- let $hdr = createElement2(["button", head_attrs, text_node(head_text)]);
686
+ /* A section head reads as a line: its badge follows the label */
687
+ let $hdr = createElement2(["button", head_attrs,
688
+ [text_node(head_text), item_badge_node(gobj, it, false)]]);
648
689
  $container.appendChild($hdr);
649
690
 
650
691
  let $ul = createElement2(
@@ -696,16 +737,21 @@ function item_li(gobj, it, opts)
696
737
  let { icon_pos, show_label, stacked } = opts;
697
738
  let children = [];
698
739
  let label = it.name || "";
740
+ let is_stacked = !!(stacked || icon_pos === "top" || icon_pos === "bottom");
699
741
 
700
- let icon_el = !empty_string(it.icon)
701
- ? ["span", {class: "icon"}, ["i", {class: it.icon, "aria-hidden":"true"}]]
702
- : null;
703
742
  let label_el = (show_label && !empty_string(label))
704
743
  ? ["span", {class: "yui-nav-label", i18n: label}, text_node(label)]
705
744
  : null;
745
+ /* On the corner of the icon where the item is an icon over its
746
+ * label or an icon alone; after the label where it reads as a line. */
747
+ let pinned = !empty_string(it.icon) && (is_stacked || !label_el);
748
+ let badge_el = item_badge_node(gobj, it, pinned);
749
+ let icon_el = !empty_string(it.icon)
750
+ ? icon_span(it.icon, "icon", pinned ? badge_el : null)
751
+ : null;
706
752
 
707
753
  let a_class = "yui-nav-item";
708
- if(stacked || icon_pos === "top" || icon_pos === "bottom") {
754
+ if(is_stacked) {
709
755
  a_class += " yui-nav-stacked";
710
756
  }
711
757
 
@@ -713,6 +759,9 @@ function item_li(gobj, it, opts)
713
759
  if(label_el) {
714
760
  children.push(label_el);
715
761
  }
762
+ if(!pinned) {
763
+ children.push(badge_el);
764
+ }
716
765
  if(icon_el) {
717
766
  children.push(icon_el);
718
767
  }
@@ -723,6 +772,9 @@ function item_li(gobj, it, opts)
723
772
  if(label_el) {
724
773
  children.push(label_el);
725
774
  }
775
+ if(!pinned) {
776
+ children.push(badge_el);
777
+ }
726
778
  }
727
779
 
728
780
  let aria_key = label || it.id;
@@ -765,9 +817,12 @@ function item_iconbar(gobj, it, opts)
765
817
  {
766
818
  let { icon_pos, show_label } = opts;
767
819
  let label = it.name || "";
768
- let icon_el = !empty_string(it.icon)
769
- ? ["span", {class: "icon is-medium"},
770
- ["i", {class: it.icon, "aria-hidden":"true"}]]
820
+ /* An icon bar item is an icon over its label: the badge goes on
821
+ * the corner of the icon, or after the label of one that has none. */
822
+ let pinned = !empty_string(it.icon);
823
+ let badge_el = item_badge_node(gobj, it, pinned);
824
+ let icon_el = pinned
825
+ ? icon_span(it.icon, "icon is-medium", badge_el)
771
826
  : null;
772
827
  let label_el = (show_label && !empty_string(label))
773
828
  ? ["span", {class: "yui-nav-label is-size-7", i18n: label}, text_node(label)]
@@ -789,6 +844,9 @@ function item_iconbar(gobj, it, opts)
789
844
  children.push(label_el);
790
845
  }
791
846
  }
847
+ if(!pinned) {
848
+ children.push(badge_el);
849
+ }
792
850
 
793
851
  let a_attrs = {
794
852
  class: "yui-nav-item yui-nav-stacked",
@@ -810,6 +868,41 @@ function item_iconbar(gobj, it, opts)
810
868
  ];
811
869
  }
812
870
 
871
+ /************************************************************
872
+ * The badge of an item (yui_badge.js), always drawn -- hidden
873
+ * when empty -- so EV_SET_ITEM_BADGE has a node to write into.
874
+ * What was set at runtime for its id (`badges`) wins over the
875
+ * item's own `badge` / `badge_tone`. `pinned`: on the corner of
876
+ * the icon instead of after the label.
877
+ ************************************************************/
878
+ function item_badge_node(gobj, it, pinned)
879
+ {
880
+ let badges = gobj_read_attr(gobj, "badges");
881
+ let state = it;
882
+ if(is_object(badges) && is_string(it.id) &&
883
+ Object.prototype.hasOwnProperty.call(badges, it.id)) {
884
+ state = badges[it.id] || {};
885
+ }
886
+ let classes = "NAV_ITEM_BADGE yui-badge yui-nav-badge";
887
+ if(pinned) {
888
+ classes += " yui-nav-badge-pinned";
889
+ }
890
+ return badge_node(state.badge, state.badge_tone, classes,
891
+ `C_YUI_NAV '${gobj_read_attr(gobj, "menu_id") || ""}': item '${it.id}'`);
892
+ }
893
+
894
+ /************************************************************
895
+ * An item's icon, with its badge on the corner when given one.
896
+ ************************************************************/
897
+ function icon_span(icon, classes, badge_el)
898
+ {
899
+ let glyph = ["i", {class: icon, "aria-hidden": "true"}];
900
+ if(!badge_el) {
901
+ return ["span", {class: classes}, glyph];
902
+ }
903
+ return ["span", {class: `${classes} yui-nav-icon`}, [glyph, badge_el]];
904
+ }
905
+
813
906
  function wire_clicks(gobj, $container)
814
907
  {
815
908
  let priv = gobj.priv;
@@ -932,6 +1025,44 @@ function ac_set_items(gobj, event, kw, src)
932
1025
  return 0;
933
1026
  }
934
1027
 
1028
+ /************************************************************
1029
+ * EV_SET_ITEM_BADGE {item_id, badge, badge_tone} — the badge of
1030
+ * one item, set at runtime (yui_shell_set_nav_item_badge).
1031
+ *
1032
+ * Kept in `badges`, so every later rebuild draws it, and painted
1033
+ * in place: no rebuild for a count that moves on every tick. An
1034
+ * id this nav does not draw is kept all the same — a later
1035
+ * EV_SET_ITEMS may bring the item.
1036
+ ************************************************************/
1037
+ function ac_set_item_badge(gobj, event, kw, src)
1038
+ {
1039
+ let item_id = kw.item_id;
1040
+ if(empty_string(item_id)) {
1041
+ log_error(`C_YUI_NAV: ${event} without item_id, from ${gobj_short_name(src)}`);
1042
+ return -1;
1043
+ }
1044
+ let menu_id = gobj_read_attr(gobj, "menu_id") || "";
1045
+ let tone = badge_tone(kw.badge_tone, `C_YUI_NAV '${menu_id}': item '${item_id}'`);
1046
+ let badge = (kw.badge === undefined) ? null : kw.badge;
1047
+
1048
+ /* A new dict: the one in the attr may be the creator's. */
1049
+ let badges = Object.assign({}, gobj_read_attr(gobj, "badges"));
1050
+ badges[item_id] = {badge: badge, badge_tone: tone};
1051
+ gobj_write_attr(gobj, "badges", badges);
1052
+
1053
+ let $c = gobj_read_attr(gobj, "$container");
1054
+ if(!$c) {
1055
+ return 0;
1056
+ }
1057
+ let $badges = $c.querySelectorAll(
1058
+ `[data-item-id="${css_escape(item_id)}"] .NAV_ITEM_BADGE`
1059
+ );
1060
+ for(let $badge of $badges) {
1061
+ paint_badge($badge, badge, tone);
1062
+ }
1063
+ return 0;
1064
+ }
1065
+
935
1066
  /************************************************************
936
1067
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
937
1068
  ************************************************************/
@@ -1043,14 +1174,16 @@ function create_gclass(gclass_name)
1043
1174
 
1044
1175
  const states = [
1045
1176
  ["ST_IDLE", [
1046
- ["EV_ROUTE_CHANGED", ac_route_changed, null],
1047
- ["EV_SET_ITEMS", ac_set_items, null]
1177
+ ["EV_ROUTE_CHANGED", ac_route_changed, null],
1178
+ ["EV_SET_ITEMS", ac_set_items, null],
1179
+ ["EV_SET_ITEM_BADGE", ac_set_item_badge, null]
1048
1180
  ]]
1049
1181
  ];
1050
1182
 
1051
1183
  const event_types = [
1052
1184
  ["EV_ROUTE_CHANGED", 0],
1053
1185
  ["EV_SET_ITEMS", 0],
1186
+ ["EV_SET_ITEM_BADGE", 0],
1054
1187
  ["EV_NAV_CLICKED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT],
1055
1188
  ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT],
1056
1189
  ["EV_DRAWER_CLOSE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT]
@@ -459,29 +459,30 @@
459
459
  }
460
460
 
461
461
  /*------------------------------------------------------------
462
- * Toolbar badge — the count pinned to an item's icon
462
+ * Badge — the count of a toolbar item or a menu item
463
+ * (yui_badge.js)
463
464
  *
464
- * Anchored to the ICON and not to the button: with its label on
465
- * (desktop) the button is wide, and a badge in its corner would
466
- * float away from the glyph it counts.
465
+ * `.yui-badge` is the LOOK, one for both. Where it SITS is the
466
+ * host's: `.yui-toolbar-badge` on the corner of a toolbar icon,
467
+ * `.yui-nav-badge` after a menu item's label, or on the corner
468
+ * of its icon (`.yui-nav-badge-pinned`).
469
+ *
470
+ * Anchored to the ICON and not to the control: with its label on
471
+ * (desktop) a toolbar button is wide, and a badge in its corner
472
+ * would float away from the glyph it counts.
467
473
  *
468
474
  * `hidden` is honoured explicitly: the element is always in the
469
475
  * DOM (so the runtime setter has somewhere to write) and
470
476
  * [hidden] loses to `display:inline-flex` without this rule.
471
477
  *
472
- * Danger colour on purpose — a badge exists to pull the eye. It
473
- * is its own colour pair in both schemes rather than a Bulma
474
- * helper, so it keeps its contrast when the toolbar background
475
- * changes with the theme. No transition: house rule.
478
+ * Danger colour by default, on purpose — a badge exists to pull
479
+ * the eye. A TONE (`is-warning`, `is-info`, …) is a FILL: the
480
+ * raw Bulma token under its `-invert` ink, its own colour pair in
481
+ * both schemes rather than a Bulma helper, so the badge keeps its
482
+ * contrast when the bar under it changes with the theme (README,
483
+ * "Colour"). No transition: house rule.
476
484
  *------------------------------------------------------------*/
477
- .yui-toolbar-icon {
478
- position: relative;
479
- overflow: visible;
480
- }
481
- .yui-toolbar-badge {
482
- position: absolute;
483
- top: -0.35em;
484
- right: -0.55em;
485
+ .yui-badge {
485
486
  display: inline-flex;
486
487
  align-items: center;
487
488
  justify-content: center;
@@ -491,19 +492,84 @@
491
492
  height: 1.15em;
492
493
  padding: 0 0.3em;
493
494
  border-radius: 999px;
494
- font-size: 0.62em;
495
495
  font-weight: 700;
496
496
  line-height: 1;
497
497
  font-family: inherit;
498
+ white-space: nowrap;
498
499
  background-color: var(--bulma-danger, #f14668);
499
500
  color: var(--bulma-danger-invert, #fff);
500
501
  /* Separates the pill from the glyph when they overlap. */
501
502
  box-shadow: 0 0 0 0.12em var(--bulma-scheme-main, #fff);
502
503
  pointer-events: none;
503
504
  }
504
- .yui-toolbar-badge[hidden] {
505
+ .yui-badge[hidden] {
505
506
  display: none;
506
507
  }
508
+ .yui-badge.is-danger {
509
+ background-color: var(--bulma-danger, #f14668);
510
+ color: var(--bulma-danger-invert, #fff);
511
+ }
512
+ .yui-badge.is-warning {
513
+ background-color: var(--bulma-warning, #ffb70f);
514
+ color: var(--bulma-warning-invert, rgba(0, 0, 0, 0.7));
515
+ }
516
+ .yui-badge.is-info {
517
+ background-color: var(--bulma-info, #66d1ff);
518
+ color: var(--bulma-info-invert, rgba(0, 0, 0, 0.7));
519
+ }
520
+ .yui-badge.is-success {
521
+ background-color: var(--bulma-success, #48c78e);
522
+ color: var(--bulma-success-invert, rgba(0, 0, 0, 0.7));
523
+ }
524
+ .yui-badge.is-primary {
525
+ background-color: var(--bulma-primary, #00d1b2);
526
+ color: var(--bulma-primary-invert, rgba(0, 0, 0, 0.7));
527
+ }
528
+ .yui-badge.is-link {
529
+ background-color: var(--bulma-link, #485fc7);
530
+ color: var(--bulma-link-invert, #fff);
531
+ }
532
+
533
+ /* Toolbar: on the corner of the icon, sized by the icon. */
534
+ .yui-toolbar-icon {
535
+ position: relative;
536
+ overflow: visible;
537
+ }
538
+ .yui-toolbar-badge {
539
+ position: absolute;
540
+ top: -0.35em;
541
+ right: -0.55em;
542
+ font-size: 0.62em;
543
+ }
544
+
545
+ /* Menu item: after the label. Sized in rem, so it is the same
546
+ * pill in every layout whatever the item's own font size. */
547
+ .yui-nav-badge {
548
+ flex: 0 0 auto;
549
+ margin-inline-start: 0.4rem;
550
+ font-size: 0.75rem;
551
+ vertical-align: middle;
552
+ }
553
+ /* Where the item already spaces its children (a flex row or column
554
+ * with a gap), the gap is the space. */
555
+ .yui-nav-vertical .menu-list a > .yui-nav-badge,
556
+ .yui-nav-stacked > .yui-nav-badge,
557
+ .yui-nav-cards .yui-nav-card > .yui-nav-badge {
558
+ margin-inline-start: 0;
559
+ }
560
+ /* ...or on the corner of the icon, where the item is an icon over
561
+ * its label (icon bar, cards, a stacked item) or an icon alone. */
562
+ .yui-nav-icon {
563
+ position: relative;
564
+ overflow: visible;
565
+ }
566
+ .yui-nav-badge.yui-nav-badge-pinned {
567
+ position: absolute;
568
+ top: -0.35em;
569
+ inset-inline-end: -0.55em;
570
+ margin: 0;
571
+ font-size: 0.7rem;
572
+ }
507
573
 
508
574
  /*============================================================
509
575
  * Toolbar — type:"brand"