@yuneta/gobj-ui 5.5.0 → 5.7.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 +40 -0
- package/dist/gobj-ui.cjs.js +319 -149
- package/dist/gobj-ui.es.js +319 -150
- package/index.js +1 -0
- package/package.json +2 -2
- package/src/c_yui_form.js +100 -75
- package/src/c_yui_nav.js +6 -1
- package/src/c_yui_shell.css +47 -0
- package/src/c_yui_shell.js +106 -2
- package/src/form_toolbar_plan.js +59 -0
- package/src/form_toolbar_plan.test.js +54 -0
package/src/c_yui_form.js
CHANGED
|
@@ -57,6 +57,7 @@ import { createJSONEditor } from 'vanilla-jsoneditor';
|
|
|
57
57
|
import "vanilla-jsoneditor/themes/jse-theme-dark.css";
|
|
58
58
|
import "./c_yui_form.css";
|
|
59
59
|
import {attach_clear, refresh_clear} from "./yui_inputs.js";
|
|
60
|
+
import {plan_toolbar} from "./form_toolbar_plan.js";
|
|
60
61
|
/* Read at field-build time only — deliberately NOT watched: the hosting
|
|
61
62
|
* dialog rebuilds the form on every open, and re-rendering a form under
|
|
62
63
|
* the user mid-edit would throw away what they typed. */
|
|
@@ -96,6 +97,20 @@ SDATA(data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable
|
|
|
96
97
|
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
|
|
97
98
|
SDATA(data_type_t.DTP_BOOLEAN, "with_rownum", 0, false, "Print first column with row number"),
|
|
98
99
|
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_row_btn", 0, false, "Print last column with delete row button"),
|
|
100
|
+
/*
|
|
101
|
+
* Which buttons the bottom toolbar shows, and in which order. The
|
|
102
|
+
* default is the five it has always shown, so nothing moves for a
|
|
103
|
+
* caller that does not set it.
|
|
104
|
+
*
|
|
105
|
+
* It exists because the toolbar was hardcoded: a three-field sign-up
|
|
106
|
+
* dialog with ONE action still got save + undo + clear + copy + paste,
|
|
107
|
+
* and the only way out was to not use this gclass at all. `[]` hides
|
|
108
|
+
* the toolbar entirely, for a caller that supplies its own buttons.
|
|
109
|
+
*
|
|
110
|
+
* Unknown names are ignored, with a warning: a typo must not silently
|
|
111
|
+
* drop the save button.
|
|
112
|
+
*/
|
|
113
|
+
SDATA(data_type_t.DTP_JSON, "toolbar", 0, ["save", "undo", "clear", "copy", "paste"], "Toolbar buttons to show, in order: save, undo, clear, copy, paste. [] hides the toolbar"),
|
|
99
114
|
/*
|
|
100
115
|
* Defaults for tabulator
|
|
101
116
|
*/
|
|
@@ -283,6 +298,90 @@ async function readClipboard(gobj)
|
|
|
283
298
|
}
|
|
284
299
|
}
|
|
285
300
|
|
|
301
|
+
/************************************************************
|
|
302
|
+
* Bottom toolbar, from the `toolbar` attr.
|
|
303
|
+
*
|
|
304
|
+
* The five buttons it can show, and their order, are the
|
|
305
|
+
* caller's: a dialog with a single action asks for ["save"],
|
|
306
|
+
* and [] leaves no toolbar at all. Default is the five it
|
|
307
|
+
* always had, so nothing moves for a caller that says nothing.
|
|
308
|
+
************************************************************/
|
|
309
|
+
function toolbar_button(gobj, name)
|
|
310
|
+
{
|
|
311
|
+
switch(name) {
|
|
312
|
+
case "save":
|
|
313
|
+
return ['button', {class: 'button p-1 button-save', title: 'save', 'aria-label': 'save', disabled: true}, [
|
|
314
|
+
['span', {class: 'icon m-0'}, '<i class="yi-floppy-disk"></i>'],
|
|
315
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'save'}, 'save']
|
|
316
|
+
], {
|
|
317
|
+
click: function(evt) {
|
|
318
|
+
evt.stopPropagation();
|
|
319
|
+
gobj_send_event(gobj, "EV_SAVE_RECORD", {}, gobj);
|
|
320
|
+
}
|
|
321
|
+
}];
|
|
322
|
+
case "undo":
|
|
323
|
+
return ['button', {class: 'button p-1 button-undo', title: 'undo', 'aria-label': 'undo', disabled: true}, [
|
|
324
|
+
['span', {class: 'icon m-0'}, '<i class="yi-arrow-rotate-left"></i>'],
|
|
325
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'undo'}, 'undo']
|
|
326
|
+
], {
|
|
327
|
+
click: function(evt) {
|
|
328
|
+
evt.stopPropagation();
|
|
329
|
+
gobj_send_event(gobj, "EV_UNDO_RECORD", {}, gobj);
|
|
330
|
+
}
|
|
331
|
+
}];
|
|
332
|
+
case "clear":
|
|
333
|
+
return ['button', {class: 'button p-1', title: 'clear', 'aria-label': 'clear'}, [
|
|
334
|
+
['span', {class: 'icon m-0'}, '<i class="yi-broom-wide"></i>'],
|
|
335
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'clear'}, 'clear']
|
|
336
|
+
], {
|
|
337
|
+
click: function(evt) {
|
|
338
|
+
evt.stopPropagation();
|
|
339
|
+
gobj_send_event(gobj, "EV_CLEAR_RECORD", {}, gobj);
|
|
340
|
+
}
|
|
341
|
+
}];
|
|
342
|
+
case "copy":
|
|
343
|
+
return ['button', {class: 'button p-1', title: 'copy', 'aria-label': 'copy'}, [
|
|
344
|
+
['span', {class: 'icon m-0'}, '<i class="yi-copy"></i>'],
|
|
345
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'copy'}, 'copy']
|
|
346
|
+
], {click: function(evt) {
|
|
347
|
+
evt.stopPropagation();
|
|
348
|
+
gobj_send_event(gobj, "EV_COPY_RECORD", {}, gobj);
|
|
349
|
+
}
|
|
350
|
+
}];
|
|
351
|
+
case "paste":
|
|
352
|
+
return ['button', {class: 'button p-1', title: 'paste', 'aria-label': 'paste'}, [
|
|
353
|
+
['span', {class: 'icon m-0'}, '<i class="yi-paste"></i>'],
|
|
354
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'paste'}, 'paste']
|
|
355
|
+
], {click: async function(evt) {
|
|
356
|
+
evt.stopPropagation();
|
|
357
|
+
await readClipboard(gobj);
|
|
358
|
+
}
|
|
359
|
+
}];
|
|
360
|
+
default:
|
|
361
|
+
return null; /* plan_toolbar() already warned */
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function build_toolbar(gobj)
|
|
366
|
+
{
|
|
367
|
+
const plan = plan_toolbar(gobj_read_attr(gobj, "toolbar"));
|
|
368
|
+
|
|
369
|
+
for(let name of plan.unknown) {
|
|
370
|
+
/* A typo must not silently drop the save button. */
|
|
371
|
+
log_warning(`${gobj_short_name(gobj)}: unknown toolbar button '${name}'`);
|
|
372
|
+
}
|
|
373
|
+
if(!plan.left.length && !plan.right.length) {
|
|
374
|
+
return ['span', {class: 'yui-toolbar-form-empty', style: 'display:none;'}, ''];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return ['div', {class: 'yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center', style: 'width:100%; flex-wrap:wrap; row-gap:6px;'}, [
|
|
378
|
+
['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'},
|
|
379
|
+
plan.left.map((n) => toolbar_button(gobj, n))],
|
|
380
|
+
['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'},
|
|
381
|
+
plan.right.map((n) => toolbar_button(gobj, n))]
|
|
382
|
+
]];
|
|
383
|
+
}
|
|
384
|
+
|
|
286
385
|
/************************************************************
|
|
287
386
|
* Build UI
|
|
288
387
|
************************************************************/
|
|
@@ -314,81 +413,7 @@ function build_ui(gobj)
|
|
|
314
413
|
/*----------------------------*
|
|
315
414
|
* Bottom toolbar
|
|
316
415
|
*----------------------------*/
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
/*----------------------------*
|
|
320
|
-
* Left buttons
|
|
321
|
-
*----------------------------*/
|
|
322
|
-
['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'}, [
|
|
323
|
-
/*----------------------------*
|
|
324
|
-
* Save
|
|
325
|
-
*----------------------------*/
|
|
326
|
-
['button', {class: 'button p-1 button-save', title: 'save', 'aria-label': 'save', disabled: true}, [
|
|
327
|
-
['span', {class: 'icon m-0'}, '<i class="yi-floppy-disk"></i>'],
|
|
328
|
-
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'save'}, 'save']
|
|
329
|
-
], {
|
|
330
|
-
click: function(evt) {
|
|
331
|
-
evt.stopPropagation();
|
|
332
|
-
gobj_send_event(gobj, "EV_SAVE_RECORD", {}, gobj);
|
|
333
|
-
}
|
|
334
|
-
}],
|
|
335
|
-
|
|
336
|
-
/*----------------------------*
|
|
337
|
-
* Undo
|
|
338
|
-
*----------------------------*/
|
|
339
|
-
['button', {class: 'button p-1 button-undo', title: 'undo', 'aria-label': 'undo', disabled: true}, [
|
|
340
|
-
['span', {class: 'icon m-0'}, '<i class="yi-arrow-rotate-left"></i>'],
|
|
341
|
-
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'undo'}, 'undo']
|
|
342
|
-
], {
|
|
343
|
-
click: function(evt) {
|
|
344
|
-
evt.stopPropagation();
|
|
345
|
-
gobj_send_event(gobj, "EV_UNDO_RECORD", {}, gobj);
|
|
346
|
-
}
|
|
347
|
-
}],
|
|
348
|
-
|
|
349
|
-
/*----------------------------*
|
|
350
|
-
* Clear
|
|
351
|
-
*----------------------------*/
|
|
352
|
-
['button', {class: 'button p-1', title: 'clear', 'aria-label': 'clear'}, [
|
|
353
|
-
['span', {class: 'icon m-0'}, '<i class="yi-broom-wide"></i>'],
|
|
354
|
-
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'clear'}, 'clear']
|
|
355
|
-
], {
|
|
356
|
-
click: function(evt) {
|
|
357
|
-
evt.stopPropagation();
|
|
358
|
-
gobj_send_event(gobj, "EV_CLEAR_RECORD", {}, gobj);
|
|
359
|
-
}
|
|
360
|
-
}],
|
|
361
|
-
]],
|
|
362
|
-
|
|
363
|
-
/*----------------------------*
|
|
364
|
-
* Right buttons
|
|
365
|
-
*----------------------------*/
|
|
366
|
-
['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'}, [
|
|
367
|
-
/*----------------------------*
|
|
368
|
-
* Copy
|
|
369
|
-
*----------------------------*/
|
|
370
|
-
['button', {class: 'button p-1', title: 'copy', 'aria-label': 'copy'}, [
|
|
371
|
-
['span', {class: 'icon m-0'}, '<i class="yi-copy"></i>'],
|
|
372
|
-
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'copy'}, 'copy']
|
|
373
|
-
], {click: function(evt) {
|
|
374
|
-
evt.stopPropagation();
|
|
375
|
-
gobj_send_event(gobj, "EV_COPY_RECORD", {}, gobj);
|
|
376
|
-
}
|
|
377
|
-
}],
|
|
378
|
-
|
|
379
|
-
/*----------------------------*
|
|
380
|
-
* Paste
|
|
381
|
-
*----------------------------*/
|
|
382
|
-
['button', {class: 'button p-1', title: 'paste', 'aria-label': 'paste'}, [
|
|
383
|
-
['span', {class: 'icon m-0'}, '<i class="yi-paste"></i>'],
|
|
384
|
-
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'paste'}, 'paste']
|
|
385
|
-
], {click: async function(evt) {
|
|
386
|
-
evt.stopPropagation();
|
|
387
|
-
await readClipboard(gobj);
|
|
388
|
-
}
|
|
389
|
-
}]
|
|
390
|
-
]]
|
|
391
|
-
]]
|
|
416
|
+
build_toolbar(gobj)
|
|
392
417
|
]]
|
|
393
418
|
);
|
|
394
419
|
|
package/src/c_yui_nav.js
CHANGED
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
* container; build_ui re-applies them on every rebuild.
|
|
33
33
|
*
|
|
34
34
|
* Each item supports:
|
|
35
|
-
* id, name, icon (CSS class or svg id), route,
|
|
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
41
|
* class — extra CSS class(es) on the item (tabs layout), e.g.
|
|
37
42
|
* a per-item state colour like "yui-nav-disconnected"
|
|
38
43
|
* closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
|
package/src/c_yui_shell.css
CHANGED
|
@@ -449,6 +449,53 @@
|
|
|
449
449
|
background-color: var(--bulma-scheme-main-bis, rgba(0,0,0,0.04));
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
/*------------------------------------------------------------
|
|
453
|
+
* Toolbar badge — the count pinned to an item's icon
|
|
454
|
+
*
|
|
455
|
+
* Anchored to the ICON and not to the button: with its label on
|
|
456
|
+
* (desktop) the button is wide, and a badge in its corner would
|
|
457
|
+
* float away from the glyph it counts.
|
|
458
|
+
*
|
|
459
|
+
* `hidden` is honoured explicitly: the element is always in the
|
|
460
|
+
* DOM (so the runtime setter has somewhere to write) and
|
|
461
|
+
* [hidden] loses to `display:inline-flex` without this rule.
|
|
462
|
+
*
|
|
463
|
+
* Danger colour on purpose — a badge exists to pull the eye. It
|
|
464
|
+
* is its own colour pair in both schemes rather than a Bulma
|
|
465
|
+
* helper, so it keeps its contrast when the toolbar background
|
|
466
|
+
* changes with the theme. No transition: house rule.
|
|
467
|
+
*------------------------------------------------------------*/
|
|
468
|
+
.yui-toolbar-icon {
|
|
469
|
+
position: relative;
|
|
470
|
+
overflow: visible;
|
|
471
|
+
}
|
|
472
|
+
.yui-toolbar-badge {
|
|
473
|
+
position: absolute;
|
|
474
|
+
top: -0.35em;
|
|
475
|
+
right: -0.55em;
|
|
476
|
+
display: inline-flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
justify-content: center;
|
|
479
|
+
box-sizing: border-box;
|
|
480
|
+
/* A round dot for one digit, a pill from two on. */
|
|
481
|
+
min-width: 1.15em;
|
|
482
|
+
height: 1.15em;
|
|
483
|
+
padding: 0 0.3em;
|
|
484
|
+
border-radius: 999px;
|
|
485
|
+
font-size: 0.62em;
|
|
486
|
+
font-weight: 700;
|
|
487
|
+
line-height: 1;
|
|
488
|
+
font-family: inherit;
|
|
489
|
+
background-color: var(--bulma-danger, #f14668);
|
|
490
|
+
color: var(--bulma-danger-invert, #fff);
|
|
491
|
+
/* Separates the pill from the glyph when they overlap. */
|
|
492
|
+
box-shadow: 0 0 0 0.12em var(--bulma-scheme-main, #fff);
|
|
493
|
+
pointer-events: none;
|
|
494
|
+
}
|
|
495
|
+
.yui-toolbar-badge[hidden] {
|
|
496
|
+
display: none;
|
|
497
|
+
}
|
|
498
|
+
|
|
452
499
|
/*============================================================
|
|
453
500
|
* Toolbar — type:"brand"
|
|
454
501
|
* Logo image + wordmark text. Renders as <button> when
|
package/src/c_yui_shell.js
CHANGED
|
@@ -1641,6 +1641,51 @@ function build_toolbar(gobj, config)
|
|
|
1641
1641
|
$zone.appendChild($bar);
|
|
1642
1642
|
}
|
|
1643
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
|
+
|
|
1644
1689
|
/************************************************************
|
|
1645
1690
|
* Renderer for the default ("action") item kind.
|
|
1646
1691
|
************************************************************/
|
|
@@ -1648,8 +1693,15 @@ function build_toolbar_action_item(gobj, it)
|
|
|
1648
1693
|
{
|
|
1649
1694
|
let children = [];
|
|
1650
1695
|
if(!empty_string(it.icon)) {
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
+
]);
|
|
1653
1705
|
}
|
|
1654
1706
|
if(!empty_string(it.name)) {
|
|
1655
1707
|
children.push(["span", {class: "yui-toolbar-item-label", i18n: it.name},
|
|
@@ -3368,6 +3420,57 @@ function yui_shell_set_toolbar_item_icon(shell_gobj, item_id, icon_class)
|
|
|
3368
3420
|
}
|
|
3369
3421
|
}
|
|
3370
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
|
+
|
|
3371
3474
|
/************************************************************
|
|
3372
3475
|
* Programmatic close of any open toolbar dropdown. Useful for
|
|
3373
3476
|
* external triggers (e.g. EV_LOGOUT firing from elsewhere) that
|
|
@@ -3414,6 +3517,7 @@ export {
|
|
|
3414
3517
|
yui_shell_language_changed,
|
|
3415
3518
|
yui_shell_set_connection_state,
|
|
3416
3519
|
yui_shell_set_toolbar_item_icon,
|
|
3520
|
+
yui_shell_set_toolbar_item_badge,
|
|
3417
3521
|
yui_shell_close_dropdown,
|
|
3418
3522
|
yui_shell_set_submenu
|
|
3419
3523
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_toolbar_plan.js
|
|
3
|
+
*
|
|
4
|
+
* Which buttons the C_YUI_FORM toolbar shows, and where.
|
|
5
|
+
*
|
|
6
|
+
* Pure, so it can be tested without a DOM: the gclass turns the
|
|
7
|
+
* plan into elements, this decides the plan.
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2026, ArtGins.
|
|
10
|
+
* All Rights Reserved.
|
|
11
|
+
***********************************************************************/
|
|
12
|
+
|
|
13
|
+
/* Save/undo/clear sit on the left of the bar, copy/paste on the right.
|
|
14
|
+
* That split is what the layout has always drawn, and keeping it means
|
|
15
|
+
* a caller who drops one group does not leave a hole in the middle. */
|
|
16
|
+
const LEFT_BUTTONS = ["save", "undo", "clear"];
|
|
17
|
+
const RIGHT_BUTTONS = ["copy", "paste"];
|
|
18
|
+
|
|
19
|
+
const DEFAULT_TOOLBAR = ["save", "undo", "clear", "copy", "paste"];
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/***************************************************************
|
|
23
|
+
* plan_toolbar(wanted) -> {left, right, unknown}
|
|
24
|
+
*
|
|
25
|
+
* wanted an array of button names, in the order asked for.
|
|
26
|
+
* Anything that is not an array means "the default
|
|
27
|
+
* five", so a caller that sets nothing keeps exactly
|
|
28
|
+
* the toolbar this gclass always had.
|
|
29
|
+
* [] means no toolbar at all.
|
|
30
|
+
*
|
|
31
|
+
* unknown names that match no button. They are reported and
|
|
32
|
+
* NOT silently dropped: a typo in the list would
|
|
33
|
+
* otherwise remove the save button with no trace.
|
|
34
|
+
***************************************************************/
|
|
35
|
+
function plan_toolbar(wanted)
|
|
36
|
+
{
|
|
37
|
+
if(!Array.isArray(wanted)) {
|
|
38
|
+
wanted = DEFAULT_TOOLBAR;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let left = [];
|
|
42
|
+
let right = [];
|
|
43
|
+
let unknown = [];
|
|
44
|
+
|
|
45
|
+
for(let name of wanted) {
|
|
46
|
+
if(LEFT_BUTTONS.indexOf(name) >= 0) {
|
|
47
|
+
left.push(name);
|
|
48
|
+
} else if(RIGHT_BUTTONS.indexOf(name) >= 0) {
|
|
49
|
+
right.push(name);
|
|
50
|
+
} else {
|
|
51
|
+
unknown.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {left, right, unknown};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
export {plan_toolbar, DEFAULT_TOOLBAR};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {plan_toolbar, DEFAULT_TOOLBAR} from "./form_toolbar_plan.js";
|
|
3
|
+
|
|
4
|
+
describe("plan_toolbar", () => {
|
|
5
|
+
|
|
6
|
+
it("says nothing -> the five buttons this gclass always had", () => {
|
|
7
|
+
const p = plan_toolbar(undefined);
|
|
8
|
+
expect(p.left).toEqual(["save", "undo", "clear"]);
|
|
9
|
+
expect(p.right).toEqual(["copy", "paste"]);
|
|
10
|
+
expect(p.unknown).toEqual([]);
|
|
11
|
+
expect(DEFAULT_TOOLBAR.length).toBe(5);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("a non-array is not an empty toolbar: it is the default", () => {
|
|
15
|
+
/* Reading a JSON attr that was never set must not silently
|
|
16
|
+
* leave a form with no save button. */
|
|
17
|
+
for(const v of [null, "save", 0, {}]) {
|
|
18
|
+
expect(plan_toolbar(v).left).toContain("save");
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("[] leaves no toolbar at all", () => {
|
|
23
|
+
const p = plan_toolbar([]);
|
|
24
|
+
expect(p.left).toEqual([]);
|
|
25
|
+
expect(p.right).toEqual([]);
|
|
26
|
+
expect(p.unknown).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("one action: a dialog asks for save only", () => {
|
|
30
|
+
const p = plan_toolbar(["save"]);
|
|
31
|
+
expect(p.left).toEqual(["save"]);
|
|
32
|
+
expect(p.right).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps the order asked for inside each side", () => {
|
|
36
|
+
const p = plan_toolbar(["clear", "save", "paste", "copy"]);
|
|
37
|
+
expect(p.left).toEqual(["clear", "save"]);
|
|
38
|
+
expect(p.right).toEqual(["paste", "copy"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("an unknown name is REPORTED, never silently dropped", () => {
|
|
42
|
+
/* A typo here would otherwise remove a button and leave no
|
|
43
|
+
* trace of why it is missing. */
|
|
44
|
+
const p = plan_toolbar(["save", "sav", "undo"]);
|
|
45
|
+
expect(p.left).toEqual(["save", "undo"]);
|
|
46
|
+
expect(p.unknown).toEqual(["sav"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("drops one whole side without disturbing the other", () => {
|
|
50
|
+
const p = plan_toolbar(["save", "undo", "clear"]);
|
|
51
|
+
expect(p.left).toEqual(["save", "undo", "clear"]);
|
|
52
|
+
expect(p.right).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
});
|