@yuneta/gobj-ui 7.23.63 → 7.23.65
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/dist/gobj-ui.cjs.js +110 -6
- package/dist/gobj-ui.es.js +110 -6
- package/package.json +1 -1
- package/src/c_yui_shell.js +78 -7
- package/src/c_yui_treedb_topic_with_form.js +78 -3
- package/src/shell_toolbar_helpers.js +12 -2
- package/src/shell_toolbar_helpers.test.js +55 -1
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -268,7 +268,8 @@ var TOOLBAR_ACTION_TYPES = [
|
|
|
268
268
|
"navigate",
|
|
269
269
|
"drawer",
|
|
270
270
|
"event",
|
|
271
|
-
"dropdown"
|
|
271
|
+
"dropdown",
|
|
272
|
+
"link"
|
|
272
273
|
];
|
|
273
274
|
/************************************************************
|
|
274
275
|
* Decide which renderer to use for a toolbar item.
|
|
@@ -337,6 +338,7 @@ function validate_action(action, owner_id) {
|
|
|
337
338
|
}
|
|
338
339
|
if (action.type === "navigate" && !_is_non_empty_string(action.route)) warnings.push(`toolbar item '${id}' (action.type:navigate) requires 'route'`);
|
|
339
340
|
if (action.type === "event" && !_is_non_empty_string(action.event)) warnings.push(`toolbar item '${id}' (action.type:event) requires 'event' name`);
|
|
341
|
+
if (action.type === "link" && !_is_non_empty_string(action.url)) warnings.push(`toolbar item '${id}' (action.type:link) requires 'url'`);
|
|
340
342
|
if (action.type === "dropdown") {
|
|
341
343
|
let nested = validate_dropdown_action(action, id);
|
|
342
344
|
for (let w of nested) warnings.push(w);
|
|
@@ -1684,6 +1686,20 @@ function safe_id(s) {
|
|
|
1684
1686
|
* drawer { type:"drawer", op:"toggle"|"open"|"close", menu_id? }
|
|
1685
1687
|
* event { type:"event", event, kw? }
|
|
1686
1688
|
* dropdown { type:"dropdown", items[] } (toolbar-only)
|
|
1689
|
+
* link { type:"link", url, target? }
|
|
1690
|
+
*
|
|
1691
|
+
* `link` leaves the application, so on a toolbar item and on the brand
|
|
1692
|
+
* it is rendered as a real <a href> rather than a <button>:
|
|
1693
|
+
* middle-click, open-in-new-tab and the url in the status bar are what
|
|
1694
|
+
* make a link recognisable as one, and a button that assigns
|
|
1695
|
+
* location.href has none of them. It is also why it does not become
|
|
1696
|
+
* an FSM event -- there is no state of this application to transition;
|
|
1697
|
+
* the page is being left. A target of "_blank" gets
|
|
1698
|
+
* rel="noopener noreferrer" for free.
|
|
1699
|
+
*
|
|
1700
|
+
* INSIDE A DROPDOWN it stays a <button>, deliberately: those rows are
|
|
1701
|
+
* role="menuitemradio" in a menu, and a link is not a choice among
|
|
1702
|
+
* options. There the dispatcher navigates.
|
|
1687
1703
|
*
|
|
1688
1704
|
* Per-item `show_on` is honoured: each rendered item is wrapped with
|
|
1689
1705
|
* the same Bulma-helper logic as zones.
|
|
@@ -1770,6 +1786,26 @@ function badge_node(value) {
|
|
|
1770
1786
|
];
|
|
1771
1787
|
}
|
|
1772
1788
|
/************************************************************
|
|
1789
|
+
* The attributes that turn a toolbar item into a real link, or
|
|
1790
|
+
* null when this item is not one. Answering null is what keeps
|
|
1791
|
+
* every caller's default (a <button> that dispatches an action)
|
|
1792
|
+
* exactly as it was.
|
|
1793
|
+
************************************************************/
|
|
1794
|
+
function toolbar_link_attrs(it) {
|
|
1795
|
+
let action = it && it.action || {};
|
|
1796
|
+
if (action.type !== "link") return null;
|
|
1797
|
+
if ((0, _yuneta_gobj_js.empty_string)(action.url)) {
|
|
1798
|
+
(0, _yuneta_gobj_js.log_warning)(`C_YUI_SHELL: toolbar item '${it.id || "?"}' has action.type "link" without a url`);
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
let attrs = { href: action.url };
|
|
1802
|
+
if (!(0, _yuneta_gobj_js.empty_string)(action.target)) {
|
|
1803
|
+
attrs.target = action.target;
|
|
1804
|
+
if (action.target === "_blank") attrs.rel = "noopener noreferrer";
|
|
1805
|
+
}
|
|
1806
|
+
return attrs;
|
|
1807
|
+
}
|
|
1808
|
+
/************************************************************
|
|
1773
1809
|
* Renderer for the default ("action") item kind.
|
|
1774
1810
|
************************************************************/
|
|
1775
1811
|
function build_toolbar_action_item(gobj, it) {
|
|
@@ -1808,12 +1844,17 @@ function build_toolbar_action_item(gobj, it) {
|
|
|
1808
1844
|
btn_attrs.title = tip;
|
|
1809
1845
|
btn_attrs["data-i18n-title"] = tip;
|
|
1810
1846
|
}
|
|
1847
|
+
let link = toolbar_link_attrs(it);
|
|
1848
|
+
if (link) {
|
|
1849
|
+
delete btn_attrs.type;
|
|
1850
|
+
Object.assign(btn_attrs, link);
|
|
1851
|
+
}
|
|
1811
1852
|
let $item = (0, _yuneta_gobj_js.createElement2)([
|
|
1812
|
-
"button",
|
|
1853
|
+
link ? "a" : "button",
|
|
1813
1854
|
btn_attrs,
|
|
1814
1855
|
children
|
|
1815
1856
|
]);
|
|
1816
|
-
$item.addEventListener("click", (ev) => {
|
|
1857
|
+
if (!link) $item.addEventListener("click", (ev) => {
|
|
1817
1858
|
ev.preventDefault();
|
|
1818
1859
|
handle_toolbar_action(gobj, it, $item);
|
|
1819
1860
|
});
|
|
@@ -1845,7 +1886,11 @@ function build_toolbar_brand_item(gobj, it) {
|
|
|
1845
1886
|
attrs["aria-haspopup"] = "menu";
|
|
1846
1887
|
attrs["aria-expanded"] = "false";
|
|
1847
1888
|
}
|
|
1848
|
-
|
|
1889
|
+
let link = toolbar_link_attrs(it);
|
|
1890
|
+
if (link) {
|
|
1891
|
+
tag = "a";
|
|
1892
|
+
Object.assign(attrs, link);
|
|
1893
|
+
} else if (action_type === "") tag = "div";
|
|
1849
1894
|
else attrs.type = "button";
|
|
1850
1895
|
let $item = (0, _yuneta_gobj_js.createElement2)([
|
|
1851
1896
|
tag,
|
|
@@ -1863,7 +1908,7 @@ function build_toolbar_brand_item(gobj, it) {
|
|
|
1863
1908
|
it.wordmark
|
|
1864
1909
|
]]
|
|
1865
1910
|
]);
|
|
1866
|
-
if (action_type !== "") $item.addEventListener("click", (ev) => {
|
|
1911
|
+
if (action_type !== "" && !link) $item.addEventListener("click", (ev) => {
|
|
1867
1912
|
ev.preventDefault();
|
|
1868
1913
|
handle_toolbar_action(gobj, it, $item);
|
|
1869
1914
|
});
|
|
@@ -2035,6 +2080,12 @@ function handle_toolbar_action(gobj, item, $trigger) {
|
|
|
2035
2080
|
case "dropdown":
|
|
2036
2081
|
toggle_toolbar_dropdown(gobj, item, action, $trigger);
|
|
2037
2082
|
break;
|
|
2083
|
+
case "link":
|
|
2084
|
+
if (!(0, _yuneta_gobj_js.empty_string)(action.url)) {
|
|
2085
|
+
if (action.target === "_blank") window.open(action.url, "_blank", "noopener,noreferrer");
|
|
2086
|
+
else window.location.assign(action.url);
|
|
2087
|
+
} else (0, _yuneta_gobj_js.log_warning)(`C_YUI_SHELL: toolbar item '${item.id || "?"}' has action.type "link" without a url`);
|
|
2088
|
+
break;
|
|
2038
2089
|
default: (0, _yuneta_gobj_js.log_warning)(`C_YUI_SHELL: toolbar item '${item.id || "?"}' has no/unknown action.type`);
|
|
2039
2090
|
}
|
|
2040
2091
|
}
|
|
@@ -23812,6 +23863,8 @@ function popup_mount_layer(gobj) {
|
|
|
23812
23863
|
* Framework Method: Create
|
|
23813
23864
|
***************************************************************/
|
|
23814
23865
|
function mt_create$5(gobj) {
|
|
23866
|
+
gobj.priv.reading_files = 0;
|
|
23867
|
+
gobj.priv.read_serial = 0;
|
|
23815
23868
|
let name = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
|
|
23816
23869
|
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "table_id", "table" + name);
|
|
23817
23870
|
(0, _yuneta_gobj_js.gobj_write_attr)(gobj, "toolbar_id", "toolbar" + name);
|
|
@@ -25060,6 +25113,7 @@ function form_may_close(gobj) {
|
|
|
25060
25113
|
************************************************************/
|
|
25061
25114
|
function teardown_form_child(gobj) {
|
|
25062
25115
|
let priv = gobj.priv;
|
|
25116
|
+
priv.reading_files = 0;
|
|
25063
25117
|
if (priv.form) {
|
|
25064
25118
|
if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
|
|
25065
25119
|
(0, _yuneta_gobj_js.gobj_destroy)(priv.form);
|
|
@@ -25867,6 +25921,10 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
25867
25921
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
25868
25922
|
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
25869
25923
|
let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode");
|
|
25924
|
+
if (priv.reading_files) {
|
|
25925
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: save ignored, still reading the picked files`);
|
|
25926
|
+
return -1;
|
|
25927
|
+
}
|
|
25870
25928
|
let picked = {};
|
|
25871
25929
|
const answer = (0, _yuneta_gobj_js.gobj_command)(src, "get_picked_files", {}, gobj);
|
|
25872
25930
|
if (answer && answer.result >= 0 && answer.data) picked = answer.data;
|
|
@@ -25878,6 +25936,9 @@ function ac_form_save_record(gobj, event, kw, src) {
|
|
|
25878
25936
|
}, 0);
|
|
25879
25937
|
return 0;
|
|
25880
25938
|
}
|
|
25939
|
+
const serial = ++priv.read_serial;
|
|
25940
|
+
priv.reading_files = serial;
|
|
25941
|
+
set_form_busy(gobj, true);
|
|
25881
25942
|
Promise.all(cols.map((col) => {
|
|
25882
25943
|
return yui_file_read(picked[col]).then((read) => {
|
|
25883
25944
|
return {
|
|
@@ -25891,16 +25952,54 @@ function ac_form_save_record(gobj, event, kw, src) {
|
|
|
25891
25952
|
picks[r.col] = r.read;
|
|
25892
25953
|
});
|
|
25893
25954
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FILES_READ", {
|
|
25955
|
+
serial,
|
|
25894
25956
|
mode,
|
|
25895
25957
|
record: kw,
|
|
25896
25958
|
picks
|
|
25897
25959
|
}, gobj);
|
|
25898
25960
|
}).catch((e) => {
|
|
25899
|
-
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FILES_FAILED", {
|
|
25961
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FILES_FAILED", {
|
|
25962
|
+
serial,
|
|
25963
|
+
reason: e && e.message ? e.message : String(e)
|
|
25964
|
+
}, gobj);
|
|
25900
25965
|
});
|
|
25901
25966
|
return 0;
|
|
25902
25967
|
}
|
|
25903
25968
|
/************************************************************
|
|
25969
|
+
* The form's toolbar while a read is in flight: every button
|
|
25970
|
+
* disabled, the save button spinning. Bulma's `is-loading` is the
|
|
25971
|
+
* spinner; `disabled` is what stops the second click.
|
|
25972
|
+
************************************************************/
|
|
25973
|
+
function set_form_busy(gobj, busy) {
|
|
25974
|
+
let priv = gobj.priv;
|
|
25975
|
+
if (!priv.form) return;
|
|
25976
|
+
const $container = (0, _yuneta_gobj_js.gobj_read_attr)(priv.form, "$container");
|
|
25977
|
+
if (!$container) return;
|
|
25978
|
+
$container.querySelectorAll(".yui-toolbar-form button").forEach(($b) => {
|
|
25979
|
+
if (busy) {
|
|
25980
|
+
$b.dataset.was_disabled = $b.disabled ? "1" : "0";
|
|
25981
|
+
$b.disabled = true;
|
|
25982
|
+
} else if ($b.dataset.was_disabled !== void 0) {
|
|
25983
|
+
$b.disabled = $b.dataset.was_disabled === "1";
|
|
25984
|
+
delete $b.dataset.was_disabled;
|
|
25985
|
+
}
|
|
25986
|
+
});
|
|
25987
|
+
const $save = $container.querySelector(".yui-toolbar-form .button-save");
|
|
25988
|
+
if ($save) $save.classList.toggle("is-loading", !!busy);
|
|
25989
|
+
}
|
|
25990
|
+
/************************************************************
|
|
25991
|
+
* Is this the read the view is waiting for? A read whose form was
|
|
25992
|
+
* closed (teardown zeroes the serial) or replaced lands on nobody.
|
|
25993
|
+
************************************************************/
|
|
25994
|
+
function read_is_current(gobj, kw) {
|
|
25995
|
+
let priv = gobj.priv;
|
|
25996
|
+
if (!priv.form || !priv.reading_files || kw.serial !== priv.reading_files) {
|
|
25997
|
+
(0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: files read for a form that is gone, dropped`);
|
|
25998
|
+
return false;
|
|
25999
|
+
}
|
|
26000
|
+
return true;
|
|
26001
|
+
}
|
|
26002
|
+
/************************************************************
|
|
25904
26003
|
* The one place a write of this view leaves for the app.
|
|
25905
26004
|
************************************************************/
|
|
25906
26005
|
function publish_treedb_write(gobj, mode, record, jn_files) {
|
|
@@ -25915,6 +26014,8 @@ function publish_treedb_write(gobj, mode, record, jn_files) {
|
|
|
25915
26014
|
* {mode, record, picks} -- the bytes are here now
|
|
25916
26015
|
************************************************************/
|
|
25917
26016
|
function ac_files_read(gobj, event, kw, src) {
|
|
26017
|
+
if (!read_is_current(gobj, kw)) return -1;
|
|
26018
|
+
gobj.priv.reading_files = 0;
|
|
25918
26019
|
const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
|
|
25919
26020
|
publish_treedb_write(gobj, kw.mode, record, __files__);
|
|
25920
26021
|
close_form_dialog(gobj);
|
|
@@ -25927,6 +26028,9 @@ function ac_files_read(gobj, event, kw, src) {
|
|
|
25927
26028
|
* throw away what the person typed for a failure that is not theirs.
|
|
25928
26029
|
************************************************************/
|
|
25929
26030
|
function ac_files_failed(gobj, event, kw, src) {
|
|
26031
|
+
if (!read_is_current(gobj, kw)) return -1;
|
|
26032
|
+
gobj.priv.reading_files = 0;
|
|
26033
|
+
set_form_busy(gobj, false);
|
|
25930
26034
|
(0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot read the picked file: ${kw.reason}`);
|
|
25931
26035
|
yui_shell_confirm_ok(yui_shell_of(gobj), "cannot read the picked file", {
|
|
25932
26036
|
t: i18next.t,
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -263,7 +263,8 @@ var TOOLBAR_ACTION_TYPES = [
|
|
|
263
263
|
"navigate",
|
|
264
264
|
"drawer",
|
|
265
265
|
"event",
|
|
266
|
-
"dropdown"
|
|
266
|
+
"dropdown",
|
|
267
|
+
"link"
|
|
267
268
|
];
|
|
268
269
|
/************************************************************
|
|
269
270
|
* Decide which renderer to use for a toolbar item.
|
|
@@ -332,6 +333,7 @@ function validate_action(action, owner_id) {
|
|
|
332
333
|
}
|
|
333
334
|
if (action.type === "navigate" && !_is_non_empty_string(action.route)) warnings.push(`toolbar item '${id}' (action.type:navigate) requires 'route'`);
|
|
334
335
|
if (action.type === "event" && !_is_non_empty_string(action.event)) warnings.push(`toolbar item '${id}' (action.type:event) requires 'event' name`);
|
|
336
|
+
if (action.type === "link" && !_is_non_empty_string(action.url)) warnings.push(`toolbar item '${id}' (action.type:link) requires 'url'`);
|
|
335
337
|
if (action.type === "dropdown") {
|
|
336
338
|
let nested = validate_dropdown_action(action, id);
|
|
337
339
|
for (let w of nested) warnings.push(w);
|
|
@@ -1679,6 +1681,20 @@ function safe_id(s) {
|
|
|
1679
1681
|
* drawer { type:"drawer", op:"toggle"|"open"|"close", menu_id? }
|
|
1680
1682
|
* event { type:"event", event, kw? }
|
|
1681
1683
|
* dropdown { type:"dropdown", items[] } (toolbar-only)
|
|
1684
|
+
* link { type:"link", url, target? }
|
|
1685
|
+
*
|
|
1686
|
+
* `link` leaves the application, so on a toolbar item and on the brand
|
|
1687
|
+
* it is rendered as a real <a href> rather than a <button>:
|
|
1688
|
+
* middle-click, open-in-new-tab and the url in the status bar are what
|
|
1689
|
+
* make a link recognisable as one, and a button that assigns
|
|
1690
|
+
* location.href has none of them. It is also why it does not become
|
|
1691
|
+
* an FSM event -- there is no state of this application to transition;
|
|
1692
|
+
* the page is being left. A target of "_blank" gets
|
|
1693
|
+
* rel="noopener noreferrer" for free.
|
|
1694
|
+
*
|
|
1695
|
+
* INSIDE A DROPDOWN it stays a <button>, deliberately: those rows are
|
|
1696
|
+
* role="menuitemradio" in a menu, and a link is not a choice among
|
|
1697
|
+
* options. There the dispatcher navigates.
|
|
1682
1698
|
*
|
|
1683
1699
|
* Per-item `show_on` is honoured: each rendered item is wrapped with
|
|
1684
1700
|
* the same Bulma-helper logic as zones.
|
|
@@ -1765,6 +1781,26 @@ function badge_node(value) {
|
|
|
1765
1781
|
];
|
|
1766
1782
|
}
|
|
1767
1783
|
/************************************************************
|
|
1784
|
+
* The attributes that turn a toolbar item into a real link, or
|
|
1785
|
+
* null when this item is not one. Answering null is what keeps
|
|
1786
|
+
* every caller's default (a <button> that dispatches an action)
|
|
1787
|
+
* exactly as it was.
|
|
1788
|
+
************************************************************/
|
|
1789
|
+
function toolbar_link_attrs(it) {
|
|
1790
|
+
let action = it && it.action || {};
|
|
1791
|
+
if (action.type !== "link") return null;
|
|
1792
|
+
if (empty_string(action.url)) {
|
|
1793
|
+
log_warning(`C_YUI_SHELL: toolbar item '${it.id || "?"}' has action.type "link" without a url`);
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
let attrs = { href: action.url };
|
|
1797
|
+
if (!empty_string(action.target)) {
|
|
1798
|
+
attrs.target = action.target;
|
|
1799
|
+
if (action.target === "_blank") attrs.rel = "noopener noreferrer";
|
|
1800
|
+
}
|
|
1801
|
+
return attrs;
|
|
1802
|
+
}
|
|
1803
|
+
/************************************************************
|
|
1768
1804
|
* Renderer for the default ("action") item kind.
|
|
1769
1805
|
************************************************************/
|
|
1770
1806
|
function build_toolbar_action_item(gobj, it) {
|
|
@@ -1803,12 +1839,17 @@ function build_toolbar_action_item(gobj, it) {
|
|
|
1803
1839
|
btn_attrs.title = tip;
|
|
1804
1840
|
btn_attrs["data-i18n-title"] = tip;
|
|
1805
1841
|
}
|
|
1842
|
+
let link = toolbar_link_attrs(it);
|
|
1843
|
+
if (link) {
|
|
1844
|
+
delete btn_attrs.type;
|
|
1845
|
+
Object.assign(btn_attrs, link);
|
|
1846
|
+
}
|
|
1806
1847
|
let $item = createElement2([
|
|
1807
|
-
"button",
|
|
1848
|
+
link ? "a" : "button",
|
|
1808
1849
|
btn_attrs,
|
|
1809
1850
|
children
|
|
1810
1851
|
]);
|
|
1811
|
-
$item.addEventListener("click", (ev) => {
|
|
1852
|
+
if (!link) $item.addEventListener("click", (ev) => {
|
|
1812
1853
|
ev.preventDefault();
|
|
1813
1854
|
handle_toolbar_action(gobj, it, $item);
|
|
1814
1855
|
});
|
|
@@ -1840,7 +1881,11 @@ function build_toolbar_brand_item(gobj, it) {
|
|
|
1840
1881
|
attrs["aria-haspopup"] = "menu";
|
|
1841
1882
|
attrs["aria-expanded"] = "false";
|
|
1842
1883
|
}
|
|
1843
|
-
|
|
1884
|
+
let link = toolbar_link_attrs(it);
|
|
1885
|
+
if (link) {
|
|
1886
|
+
tag = "a";
|
|
1887
|
+
Object.assign(attrs, link);
|
|
1888
|
+
} else if (action_type === "") tag = "div";
|
|
1844
1889
|
else attrs.type = "button";
|
|
1845
1890
|
let $item = createElement2([
|
|
1846
1891
|
tag,
|
|
@@ -1858,7 +1903,7 @@ function build_toolbar_brand_item(gobj, it) {
|
|
|
1858
1903
|
it.wordmark
|
|
1859
1904
|
]]
|
|
1860
1905
|
]);
|
|
1861
|
-
if (action_type !== "") $item.addEventListener("click", (ev) => {
|
|
1906
|
+
if (action_type !== "" && !link) $item.addEventListener("click", (ev) => {
|
|
1862
1907
|
ev.preventDefault();
|
|
1863
1908
|
handle_toolbar_action(gobj, it, $item);
|
|
1864
1909
|
});
|
|
@@ -2030,6 +2075,12 @@ function handle_toolbar_action(gobj, item, $trigger) {
|
|
|
2030
2075
|
case "dropdown":
|
|
2031
2076
|
toggle_toolbar_dropdown(gobj, item, action, $trigger);
|
|
2032
2077
|
break;
|
|
2078
|
+
case "link":
|
|
2079
|
+
if (!empty_string(action.url)) {
|
|
2080
|
+
if (action.target === "_blank") window.open(action.url, "_blank", "noopener,noreferrer");
|
|
2081
|
+
else window.location.assign(action.url);
|
|
2082
|
+
} else log_warning(`C_YUI_SHELL: toolbar item '${item.id || "?"}' has action.type "link" without a url`);
|
|
2083
|
+
break;
|
|
2033
2084
|
default: log_warning(`C_YUI_SHELL: toolbar item '${item.id || "?"}' has no/unknown action.type`);
|
|
2034
2085
|
}
|
|
2035
2086
|
}
|
|
@@ -23807,6 +23858,8 @@ function popup_mount_layer(gobj) {
|
|
|
23807
23858
|
* Framework Method: Create
|
|
23808
23859
|
***************************************************************/
|
|
23809
23860
|
function mt_create$5(gobj) {
|
|
23861
|
+
gobj.priv.reading_files = 0;
|
|
23862
|
+
gobj.priv.read_serial = 0;
|
|
23810
23863
|
let name = clean_name(gobj_name(gobj));
|
|
23811
23864
|
gobj_write_attr(gobj, "table_id", "table" + name);
|
|
23812
23865
|
gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
|
|
@@ -25055,6 +25108,7 @@ function form_may_close(gobj) {
|
|
|
25055
25108
|
************************************************************/
|
|
25056
25109
|
function teardown_form_child(gobj) {
|
|
25057
25110
|
let priv = gobj.priv;
|
|
25111
|
+
priv.reading_files = 0;
|
|
25058
25112
|
if (priv.form) {
|
|
25059
25113
|
if (gobj_is_running(priv.form)) gobj_stop(priv.form);
|
|
25060
25114
|
gobj_destroy(priv.form);
|
|
@@ -25862,6 +25916,10 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
25862
25916
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
25863
25917
|
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
25864
25918
|
let mode = gobj_read_str_attr(src, "form_mode");
|
|
25919
|
+
if (priv.reading_files) {
|
|
25920
|
+
log_warning(`${gobj_short_name(gobj)}: save ignored, still reading the picked files`);
|
|
25921
|
+
return -1;
|
|
25922
|
+
}
|
|
25865
25923
|
let picked = {};
|
|
25866
25924
|
const answer = gobj_command(src, "get_picked_files", {}, gobj);
|
|
25867
25925
|
if (answer && answer.result >= 0 && answer.data) picked = answer.data;
|
|
@@ -25873,6 +25931,9 @@ function ac_form_save_record(gobj, event, kw, src) {
|
|
|
25873
25931
|
}, 0);
|
|
25874
25932
|
return 0;
|
|
25875
25933
|
}
|
|
25934
|
+
const serial = ++priv.read_serial;
|
|
25935
|
+
priv.reading_files = serial;
|
|
25936
|
+
set_form_busy(gobj, true);
|
|
25876
25937
|
Promise.all(cols.map((col) => {
|
|
25877
25938
|
return yui_file_read(picked[col]).then((read) => {
|
|
25878
25939
|
return {
|
|
@@ -25886,16 +25947,54 @@ function ac_form_save_record(gobj, event, kw, src) {
|
|
|
25886
25947
|
picks[r.col] = r.read;
|
|
25887
25948
|
});
|
|
25888
25949
|
gobj_send_event(gobj, "EV_FILES_READ", {
|
|
25950
|
+
serial,
|
|
25889
25951
|
mode,
|
|
25890
25952
|
record: kw,
|
|
25891
25953
|
picks
|
|
25892
25954
|
}, gobj);
|
|
25893
25955
|
}).catch((e) => {
|
|
25894
|
-
gobj_send_event(gobj, "EV_FILES_FAILED", {
|
|
25956
|
+
gobj_send_event(gobj, "EV_FILES_FAILED", {
|
|
25957
|
+
serial,
|
|
25958
|
+
reason: e && e.message ? e.message : String(e)
|
|
25959
|
+
}, gobj);
|
|
25895
25960
|
});
|
|
25896
25961
|
return 0;
|
|
25897
25962
|
}
|
|
25898
25963
|
/************************************************************
|
|
25964
|
+
* The form's toolbar while a read is in flight: every button
|
|
25965
|
+
* disabled, the save button spinning. Bulma's `is-loading` is the
|
|
25966
|
+
* spinner; `disabled` is what stops the second click.
|
|
25967
|
+
************************************************************/
|
|
25968
|
+
function set_form_busy(gobj, busy) {
|
|
25969
|
+
let priv = gobj.priv;
|
|
25970
|
+
if (!priv.form) return;
|
|
25971
|
+
const $container = gobj_read_attr(priv.form, "$container");
|
|
25972
|
+
if (!$container) return;
|
|
25973
|
+
$container.querySelectorAll(".yui-toolbar-form button").forEach(($b) => {
|
|
25974
|
+
if (busy) {
|
|
25975
|
+
$b.dataset.was_disabled = $b.disabled ? "1" : "0";
|
|
25976
|
+
$b.disabled = true;
|
|
25977
|
+
} else if ($b.dataset.was_disabled !== void 0) {
|
|
25978
|
+
$b.disabled = $b.dataset.was_disabled === "1";
|
|
25979
|
+
delete $b.dataset.was_disabled;
|
|
25980
|
+
}
|
|
25981
|
+
});
|
|
25982
|
+
const $save = $container.querySelector(".yui-toolbar-form .button-save");
|
|
25983
|
+
if ($save) $save.classList.toggle("is-loading", !!busy);
|
|
25984
|
+
}
|
|
25985
|
+
/************************************************************
|
|
25986
|
+
* Is this the read the view is waiting for? A read whose form was
|
|
25987
|
+
* closed (teardown zeroes the serial) or replaced lands on nobody.
|
|
25988
|
+
************************************************************/
|
|
25989
|
+
function read_is_current(gobj, kw) {
|
|
25990
|
+
let priv = gobj.priv;
|
|
25991
|
+
if (!priv.form || !priv.reading_files || kw.serial !== priv.reading_files) {
|
|
25992
|
+
log_warning(`${gobj_short_name(gobj)}: files read for a form that is gone, dropped`);
|
|
25993
|
+
return false;
|
|
25994
|
+
}
|
|
25995
|
+
return true;
|
|
25996
|
+
}
|
|
25997
|
+
/************************************************************
|
|
25899
25998
|
* The one place a write of this view leaves for the app.
|
|
25900
25999
|
************************************************************/
|
|
25901
26000
|
function publish_treedb_write(gobj, mode, record, jn_files) {
|
|
@@ -25910,6 +26009,8 @@ function publish_treedb_write(gobj, mode, record, jn_files) {
|
|
|
25910
26009
|
* {mode, record, picks} -- the bytes are here now
|
|
25911
26010
|
************************************************************/
|
|
25912
26011
|
function ac_files_read(gobj, event, kw, src) {
|
|
26012
|
+
if (!read_is_current(gobj, kw)) return -1;
|
|
26013
|
+
gobj.priv.reading_files = 0;
|
|
25913
26014
|
const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
|
|
25914
26015
|
publish_treedb_write(gobj, kw.mode, record, __files__);
|
|
25915
26016
|
close_form_dialog(gobj);
|
|
@@ -25922,6 +26023,9 @@ function ac_files_read(gobj, event, kw, src) {
|
|
|
25922
26023
|
* throw away what the person typed for a failure that is not theirs.
|
|
25923
26024
|
************************************************************/
|
|
25924
26025
|
function ac_files_failed(gobj, event, kw, src) {
|
|
26026
|
+
if (!read_is_current(gobj, kw)) return -1;
|
|
26027
|
+
gobj.priv.reading_files = 0;
|
|
26028
|
+
set_form_busy(gobj, false);
|
|
25925
26029
|
log_error(`${gobj_short_name(gobj)}: cannot read the picked file: ${kw.reason}`);
|
|
25926
26030
|
yui_shell_confirm_ok(yui_shell_of(gobj), "cannot read the picked file", {
|
|
25927
26031
|
t,
|
package/package.json
CHANGED
package/src/c_yui_shell.js
CHANGED
|
@@ -1586,6 +1586,20 @@ function safe_id(s)
|
|
|
1586
1586
|
* drawer { type:"drawer", op:"toggle"|"open"|"close", menu_id? }
|
|
1587
1587
|
* event { type:"event", event, kw? }
|
|
1588
1588
|
* dropdown { type:"dropdown", items[] } (toolbar-only)
|
|
1589
|
+
* link { type:"link", url, target? }
|
|
1590
|
+
*
|
|
1591
|
+
* `link` leaves the application, so on a toolbar item and on the brand
|
|
1592
|
+
* it is rendered as a real <a href> rather than a <button>:
|
|
1593
|
+
* middle-click, open-in-new-tab and the url in the status bar are what
|
|
1594
|
+
* make a link recognisable as one, and a button that assigns
|
|
1595
|
+
* location.href has none of them. It is also why it does not become
|
|
1596
|
+
* an FSM event -- there is no state of this application to transition;
|
|
1597
|
+
* the page is being left. A target of "_blank" gets
|
|
1598
|
+
* rel="noopener noreferrer" for free.
|
|
1599
|
+
*
|
|
1600
|
+
* INSIDE A DROPDOWN it stays a <button>, deliberately: those rows are
|
|
1601
|
+
* role="menuitemradio" in a menu, and a link is not a choice among
|
|
1602
|
+
* options. There the dispatcher navigates.
|
|
1589
1603
|
*
|
|
1590
1604
|
* Per-item `show_on` is honoured: each rendered item is wrapped with
|
|
1591
1605
|
* the same Bulma-helper logic as zones.
|
|
@@ -1703,6 +1717,35 @@ function badge_node(value)
|
|
|
1703
1717
|
return ["span", attrs, text];
|
|
1704
1718
|
}
|
|
1705
1719
|
|
|
1720
|
+
/************************************************************
|
|
1721
|
+
* The attributes that turn a toolbar item into a real link, or
|
|
1722
|
+
* null when this item is not one. Answering null is what keeps
|
|
1723
|
+
* every caller's default (a <button> that dispatches an action)
|
|
1724
|
+
* exactly as it was.
|
|
1725
|
+
************************************************************/
|
|
1726
|
+
function toolbar_link_attrs(it)
|
|
1727
|
+
{
|
|
1728
|
+
let action = (it && it.action) || {};
|
|
1729
|
+
if(action.type !== "link") {
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
if(empty_string(action.url)) {
|
|
1733
|
+
log_warning(
|
|
1734
|
+
`C_YUI_SHELL: toolbar item '${it.id||"?"}' has action.type "link" ` +
|
|
1735
|
+
`without a url`
|
|
1736
|
+
);
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
let attrs = {href: action.url};
|
|
1740
|
+
if(!empty_string(action.target)) {
|
|
1741
|
+
attrs.target = action.target;
|
|
1742
|
+
if(action.target === "_blank") {
|
|
1743
|
+
attrs.rel = "noopener noreferrer";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return attrs;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1706
1749
|
/************************************************************
|
|
1707
1750
|
* Renderer for the default ("action") item kind.
|
|
1708
1751
|
************************************************************/
|
|
@@ -1751,13 +1794,20 @@ function build_toolbar_action_item(gobj, it)
|
|
|
1751
1794
|
btn_attrs.title = tip;
|
|
1752
1795
|
btn_attrs["data-i18n-title"] = tip;
|
|
1753
1796
|
}
|
|
1797
|
+
let link = toolbar_link_attrs(it);
|
|
1798
|
+
if(link) {
|
|
1799
|
+
delete btn_attrs.type; /* meaningless, and invalid, on an <a> */
|
|
1800
|
+
Object.assign(btn_attrs, link);
|
|
1801
|
+
}
|
|
1754
1802
|
let $item = createElement2(
|
|
1755
|
-
["button", btn_attrs, children]
|
|
1803
|
+
[link? "a": "button", btn_attrs, children]
|
|
1756
1804
|
);
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1805
|
+
if(!link) {
|
|
1806
|
+
$item.addEventListener("click", ev => {
|
|
1807
|
+
ev.preventDefault();
|
|
1808
|
+
handle_toolbar_action(gobj, it, $item);
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1761
1811
|
attach_context_action(gobj, $item, it);
|
|
1762
1812
|
return $item;
|
|
1763
1813
|
}
|
|
@@ -1793,7 +1843,11 @@ function build_toolbar_brand_item(gobj, it)
|
|
|
1793
1843
|
attrs["aria-haspopup"] = "menu";
|
|
1794
1844
|
attrs["aria-expanded"] = "false";
|
|
1795
1845
|
}
|
|
1796
|
-
|
|
1846
|
+
let link = toolbar_link_attrs(it);
|
|
1847
|
+
if(link) {
|
|
1848
|
+
tag = "a";
|
|
1849
|
+
Object.assign(attrs, link);
|
|
1850
|
+
} else if(action_type === "") {
|
|
1797
1851
|
/* Passive brand: no action — render a div so it is not
|
|
1798
1852
|
* keyboard-focused and screen readers don't announce a
|
|
1799
1853
|
* pressable control. */
|
|
@@ -1807,7 +1861,7 @@ function build_toolbar_brand_item(gobj, it)
|
|
|
1807
1861
|
["span", {class: "yui-toolbar-brand-wordmark", i18n: it.wordmark},
|
|
1808
1862
|
it.wordmark]
|
|
1809
1863
|
]]);
|
|
1810
|
-
if(action_type !== "") {
|
|
1864
|
+
if(action_type !== "" && !link) {
|
|
1811
1865
|
$item.addEventListener("click", ev => {
|
|
1812
1866
|
ev.preventDefault();
|
|
1813
1867
|
handle_toolbar_action(gobj, it, $item);
|
|
@@ -2032,6 +2086,23 @@ function handle_toolbar_action(gobj, item, $trigger)
|
|
|
2032
2086
|
case "dropdown":
|
|
2033
2087
|
toggle_toolbar_dropdown(gobj, item, action, $trigger);
|
|
2034
2088
|
break;
|
|
2089
|
+
case "link":
|
|
2090
|
+
/* Reached only from the renderers that stay buttons (avatar,
|
|
2091
|
+
* and the entries inside a dropdown panel). The anchor
|
|
2092
|
+
* renderers never come through here. */
|
|
2093
|
+
if(!empty_string(action.url)) {
|
|
2094
|
+
if(action.target === "_blank") {
|
|
2095
|
+
window.open(action.url, "_blank", "noopener,noreferrer");
|
|
2096
|
+
} else {
|
|
2097
|
+
window.location.assign(action.url);
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
log_warning(
|
|
2101
|
+
`C_YUI_SHELL: toolbar item '${item.id||"?"}' has action.type ` +
|
|
2102
|
+
`"link" without a url`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
break;
|
|
2035
2106
|
default:
|
|
2036
2107
|
log_warning(
|
|
2037
2108
|
`C_YUI_SHELL: toolbar item '${item.id||"?"}' has no/unknown action.type`
|
|
@@ -394,6 +394,12 @@ function popup_mount_layer(gobj)
|
|
|
394
394
|
***************************************************************/
|
|
395
395
|
function mt_create(gobj)
|
|
396
396
|
{
|
|
397
|
+
/* 0 = not reading; otherwise the serial of the read in flight. A
|
|
398
|
+
* serial and not a flag, so that a read whose form was closed and
|
|
399
|
+
* reopened cannot land on the new form. */
|
|
400
|
+
gobj.priv.reading_files = 0;
|
|
401
|
+
gobj.priv.read_serial = 0;
|
|
402
|
+
|
|
397
403
|
let name = clean_name(gobj_name(gobj));
|
|
398
404
|
gobj_write_attr(gobj, "table_id", "table" + name);
|
|
399
405
|
gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
|
|
@@ -2219,6 +2225,7 @@ function form_may_close(gobj)
|
|
|
2219
2225
|
function teardown_form_child(gobj)
|
|
2220
2226
|
{
|
|
2221
2227
|
let priv = gobj.priv;
|
|
2228
|
+
priv.reading_files = 0; // a read in flight lands on nobody
|
|
2222
2229
|
if(priv.form) {
|
|
2223
2230
|
if(gobj_is_running(priv.form)) {
|
|
2224
2231
|
gobj_stop(priv.form);
|
|
@@ -3484,6 +3491,13 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
3484
3491
|
* files never came with the record: the form KEPT them and this is
|
|
3485
3492
|
* where they are asked for. Nothing picked is the ordinary case.
|
|
3486
3493
|
*------------------------------------------------------*/
|
|
3494
|
+
if(priv.reading_files) {
|
|
3495
|
+
/* The toolbar is disabled while a read is in flight, so this is
|
|
3496
|
+
* a keyboard shortcut or a script: said, and not a second write. */
|
|
3497
|
+
log_warning(`${gobj_short_name(gobj)}: save ignored, still reading the picked files`);
|
|
3498
|
+
return -1;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3487
3501
|
let picked = {};
|
|
3488
3502
|
const answer = gobj_command(src, "get_picked_files", {}, gobj);
|
|
3489
3503
|
if(answer && answer.result >= 0 && answer.data) {
|
|
@@ -3508,8 +3522,16 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
3508
3522
|
* notification: it enters the machine as an EVENT, never as a
|
|
3509
3523
|
* chain of callbacks that the trace cannot see. The dialog stays
|
|
3510
3524
|
* open until it lands, which is what tells the person that 40 MB
|
|
3511
|
-
* are still going up
|
|
3525
|
+
* are still going up -- and stays open BUSY: its toolbar is
|
|
3526
|
+
* disabled and the save button spins, because a second Save
|
|
3527
|
+
* during the read was a second write, and a Cancel was a save
|
|
3528
|
+
* lost without a word. The serial travels with the read: a read
|
|
3529
|
+
* whose form was closed meanwhile lands on nobody (ac_files_read).
|
|
3512
3530
|
*------------------------------------------------------*/
|
|
3531
|
+
const serial = ++priv.read_serial;
|
|
3532
|
+
priv.reading_files = serial;
|
|
3533
|
+
set_form_busy(gobj, true);
|
|
3534
|
+
|
|
3513
3535
|
Promise.all(cols.map((col) => {
|
|
3514
3536
|
return yui_file_read(picked[col]).then((read) => {
|
|
3515
3537
|
return {col: col, read: read};
|
|
@@ -3520,17 +3542,61 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
3520
3542
|
picks[r.col] = r.read;
|
|
3521
3543
|
});
|
|
3522
3544
|
gobj_send_event(gobj, "EV_FILES_READ",
|
|
3523
|
-
{mode: mode, record: kw, picks: picks}, gobj
|
|
3545
|
+
{serial: serial, mode: mode, record: kw, picks: picks}, gobj
|
|
3524
3546
|
);
|
|
3525
3547
|
}).catch((e) => {
|
|
3526
3548
|
gobj_send_event(gobj, "EV_FILES_FAILED",
|
|
3527
|
-
{reason: (e && e.message)? e.message: String(e)}, gobj
|
|
3549
|
+
{serial: serial, reason: (e && e.message)? e.message: String(e)}, gobj
|
|
3528
3550
|
);
|
|
3529
3551
|
});
|
|
3530
3552
|
|
|
3531
3553
|
return 0;
|
|
3532
3554
|
}
|
|
3533
3555
|
|
|
3556
|
+
/************************************************************
|
|
3557
|
+
* The form's toolbar while a read is in flight: every button
|
|
3558
|
+
* disabled, the save button spinning. Bulma's `is-loading` is the
|
|
3559
|
+
* spinner; `disabled` is what stops the second click.
|
|
3560
|
+
************************************************************/
|
|
3561
|
+
function set_form_busy(gobj, busy)
|
|
3562
|
+
{
|
|
3563
|
+
let priv = gobj.priv;
|
|
3564
|
+
if(!priv.form) {
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
const $container = gobj_read_attr(priv.form, "$container");
|
|
3568
|
+
if(!$container) {
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
$container.querySelectorAll('.yui-toolbar-form button').forEach(($b) => {
|
|
3572
|
+
if(busy) {
|
|
3573
|
+
$b.dataset.was_disabled = $b.disabled? "1": "0";
|
|
3574
|
+
$b.disabled = true;
|
|
3575
|
+
} else if($b.dataset.was_disabled !== undefined) {
|
|
3576
|
+
$b.disabled = ($b.dataset.was_disabled === "1");
|
|
3577
|
+
delete $b.dataset.was_disabled;
|
|
3578
|
+
}
|
|
3579
|
+
});
|
|
3580
|
+
const $save = $container.querySelector('.yui-toolbar-form .button-save');
|
|
3581
|
+
if($save) {
|
|
3582
|
+
$save.classList.toggle('is-loading', !!busy);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
/************************************************************
|
|
3587
|
+
* Is this the read the view is waiting for? A read whose form was
|
|
3588
|
+
* closed (teardown zeroes the serial) or replaced lands on nobody.
|
|
3589
|
+
************************************************************/
|
|
3590
|
+
function read_is_current(gobj, kw)
|
|
3591
|
+
{
|
|
3592
|
+
let priv = gobj.priv;
|
|
3593
|
+
if(!priv.form || !priv.reading_files || kw.serial !== priv.reading_files) {
|
|
3594
|
+
log_warning(`${gobj_short_name(gobj)}: files read for a form that is gone, dropped`);
|
|
3595
|
+
return false;
|
|
3596
|
+
}
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3534
3600
|
/************************************************************
|
|
3535
3601
|
* The one place a write of this view leaves for the app.
|
|
3536
3602
|
************************************************************/
|
|
@@ -3557,6 +3623,10 @@ function publish_treedb_write(gobj, mode, record, jn_files)
|
|
|
3557
3623
|
************************************************************/
|
|
3558
3624
|
function ac_files_read(gobj, event, kw, src)
|
|
3559
3625
|
{
|
|
3626
|
+
if(!read_is_current(gobj, kw)) {
|
|
3627
|
+
return -1; /* Warning already logged */
|
|
3628
|
+
}
|
|
3629
|
+
gobj.priv.reading_files = 0;
|
|
3560
3630
|
const {record, __files__} = yui_files_manifest(kw.picks, kw.record);
|
|
3561
3631
|
publish_treedb_write(gobj, kw.mode, record, __files__);
|
|
3562
3632
|
close_form_dialog(gobj);
|
|
@@ -3571,6 +3641,11 @@ function ac_files_read(gobj, event, kw, src)
|
|
|
3571
3641
|
************************************************************/
|
|
3572
3642
|
function ac_files_failed(gobj, event, kw, src)
|
|
3573
3643
|
{
|
|
3644
|
+
if(!read_is_current(gobj, kw)) {
|
|
3645
|
+
return -1; /* Warning already logged */
|
|
3646
|
+
}
|
|
3647
|
+
gobj.priv.reading_files = 0;
|
|
3648
|
+
set_form_busy(gobj, false);
|
|
3574
3649
|
log_error(`${gobj_short_name(gobj)}: cannot read the picked file: ${kw.reason}`);
|
|
3575
3650
|
/* An i18n KEY, not a sentence: the dialog renders it with `i18n`, so
|
|
3576
3651
|
* a sentence here would be a key nobody defined and would render as
|
|
@@ -19,14 +19,19 @@
|
|
|
19
19
|
* triggers an action.type on click
|
|
20
20
|
*
|
|
21
21
|
* Action types accepted on a toolbar item or on a dropdown sub-item:
|
|
22
|
-
* "navigate" | "drawer" | "event" | "dropdown"
|
|
22
|
+
* "navigate" | "drawer" | "event" | "dropdown" | "link"
|
|
23
|
+
*
|
|
24
|
+
* "link" is the one that leaves the application, and the one item
|
|
25
|
+
* rendered as a real <a href> rather than a <button>.
|
|
23
26
|
*
|
|
24
27
|
* Copyright (c) 2026, ArtGins.
|
|
25
28
|
* All Rights Reserved.
|
|
26
29
|
***********************************************************************/
|
|
27
30
|
|
|
28
31
|
export const TOOLBAR_ITEM_KINDS = ["brand", "avatar", "connection", "action"];
|
|
29
|
-
export const TOOLBAR_ACTION_TYPES = [
|
|
32
|
+
export const TOOLBAR_ACTION_TYPES = [
|
|
33
|
+
"navigate", "drawer", "event", "dropdown", "link"
|
|
34
|
+
];
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
/************************************************************
|
|
@@ -141,6 +146,11 @@ export function validate_action(action, owner_id)
|
|
|
141
146
|
`toolbar item '${id}' (action.type:event) requires 'event' name`
|
|
142
147
|
);
|
|
143
148
|
}
|
|
149
|
+
if(action.type === "link" && !_is_non_empty_string(action.url)) {
|
|
150
|
+
warnings.push(
|
|
151
|
+
`toolbar item '${id}' (action.type:link) requires 'url'`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
144
154
|
if(action.type === "dropdown") {
|
|
145
155
|
let nested = validate_dropdown_action(action, id);
|
|
146
156
|
for(let w of nested) {
|
|
@@ -203,7 +203,9 @@ test("dropdown: invalid sub-action surfaces underlying warning", () => {
|
|
|
203
203
|
*============================================================*/
|
|
204
204
|
test("kinds + action-type constants are exposed", () => {
|
|
205
205
|
expect(TOOLBAR_ITEM_KINDS).toEqual(["brand", "avatar", "connection", "action"]);
|
|
206
|
-
expect(TOOLBAR_ACTION_TYPES).toEqual(
|
|
206
|
+
expect(TOOLBAR_ACTION_TYPES).toEqual(
|
|
207
|
+
["navigate", "drawer", "event", "dropdown", "link"]
|
|
208
|
+
);
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
|
|
@@ -246,3 +248,55 @@ test("dropdown: non-boolean 'selected' warns", () => {
|
|
|
246
248
|
expect(r.length).toBe(1);
|
|
247
249
|
expect(r[0]).toContain("non-boolean");
|
|
248
250
|
});
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/*============================================================
|
|
254
|
+
* validate_action — link
|
|
255
|
+
*
|
|
256
|
+
* The action that LEAVES the application. It is the only one
|
|
257
|
+
* rendered as an <a href>, so the url is not an optional extra:
|
|
258
|
+
* without it there is no link, only a dead control.
|
|
259
|
+
*============================================================*/
|
|
260
|
+
test("validate link: ok with a url", () => {
|
|
261
|
+
let r = validate_toolbar_item({
|
|
262
|
+
id: "landing", icon: "yi-arrow-left", name: "yuneta.io",
|
|
263
|
+
action: { type: "link", url: "https://yuneta.io/" }
|
|
264
|
+
});
|
|
265
|
+
expect(r.ok).toBe(true);
|
|
266
|
+
expect(r.warnings).toEqual([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("validate link: a target is optional", () => {
|
|
270
|
+
let r = validate_toolbar_item({
|
|
271
|
+
id: "docs",
|
|
272
|
+
action: { type: "link", url: "https://doc.yuneta.io/", target: "_blank" }
|
|
273
|
+
});
|
|
274
|
+
expect(r.ok).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("validate link: missing url flags warning", () => {
|
|
278
|
+
let r = validate_toolbar_item({
|
|
279
|
+
id: "landing", action: { type: "link" }
|
|
280
|
+
});
|
|
281
|
+
expect(r.ok).toBe(false);
|
|
282
|
+
expect(r.warnings.join("\n")).toMatch(/requires 'url'/);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("validate link: empty url counts as missing", () => {
|
|
286
|
+
let r = validate_toolbar_item({
|
|
287
|
+
id: "landing", action: { type: "link", url: " " }
|
|
288
|
+
});
|
|
289
|
+
expect(r.ok).toBe(false);
|
|
290
|
+
expect(r.warnings.join("\n")).toMatch(/requires 'url'/);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("validate link: accepted inside a dropdown too", () => {
|
|
294
|
+
let r = validate_dropdown_action({
|
|
295
|
+
type: "dropdown",
|
|
296
|
+
items: [
|
|
297
|
+
{ id: "site", name: "yuneta.io",
|
|
298
|
+
action: { type: "link", url: "https://yuneta.io/" } },
|
|
299
|
+
],
|
|
300
|
+
}, "help");
|
|
301
|
+
expect(r).toEqual([]);
|
|
302
|
+
});
|