@yuneta/gobj-ui 5.6.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.
@@ -890,7 +890,7 @@ function mt_start$15(gobj) {
890
890
  if (typeof shell_cfg.remember_section_position === "boolean") (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "remember_section_position", shell_cfg.remember_section_position);
891
891
  build_item_index(gobj, config);
892
892
  instantiate_menus(gobj, config);
893
- build_toolbar(gobj, config);
893
+ build_toolbar$1(gobj, config);
894
894
  preinstantiate_eager_views(gobj);
895
895
  if ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "use_hash")) {
896
896
  priv.hash_handler = () => {
@@ -1616,7 +1616,7 @@ function safe_id(s) {
1616
1616
  * Per-item `show_on` is honoured: each rendered item is wrapped with
1617
1617
  * the same Bulma-helper logic as zones.
1618
1618
  ************************************************************/
1619
- function build_toolbar(gobj, config) {
1619
+ function build_toolbar$1(gobj, config) {
1620
1620
  let tb = config && config.toolbar;
1621
1621
  if (!tb || !(0, _yuneta_gobj_js.is_array)(tb.items)) return;
1622
1622
  let priv = gobj.priv;
@@ -5760,6 +5760,59 @@ function refresh_clear($input) {
5760
5760
  $btn.classList.toggle("is-visible", clear_visible($input));
5761
5761
  }
5762
5762
  //#endregion
5763
+ //#region src/form_toolbar_plan.js
5764
+ /***********************************************************************
5765
+ * form_toolbar_plan.js
5766
+ *
5767
+ * Which buttons the C_YUI_FORM toolbar shows, and where.
5768
+ *
5769
+ * Pure, so it can be tested without a DOM: the gclass turns the
5770
+ * plan into elements, this decides the plan.
5771
+ *
5772
+ * Copyright (c) 2026, ArtGins.
5773
+ * All Rights Reserved.
5774
+ ***********************************************************************/
5775
+ var LEFT_BUTTONS = [
5776
+ "save",
5777
+ "undo",
5778
+ "clear"
5779
+ ];
5780
+ var RIGHT_BUTTONS = ["copy", "paste"];
5781
+ var DEFAULT_TOOLBAR = [
5782
+ "save",
5783
+ "undo",
5784
+ "clear",
5785
+ "copy",
5786
+ "paste"
5787
+ ];
5788
+ /***************************************************************
5789
+ * plan_toolbar(wanted) -> {left, right, unknown}
5790
+ *
5791
+ * wanted an array of button names, in the order asked for.
5792
+ * Anything that is not an array means "the default
5793
+ * five", so a caller that sets nothing keeps exactly
5794
+ * the toolbar this gclass always had.
5795
+ * [] means no toolbar at all.
5796
+ *
5797
+ * unknown names that match no button. They are reported and
5798
+ * NOT silently dropped: a typo in the list would
5799
+ * otherwise remove the save button with no trace.
5800
+ ***************************************************************/
5801
+ function plan_toolbar(wanted) {
5802
+ if (!Array.isArray(wanted)) wanted = DEFAULT_TOOLBAR;
5803
+ let left = [];
5804
+ let right = [];
5805
+ let unknown = [];
5806
+ for (let name of wanted) if (LEFT_BUTTONS.indexOf(name) >= 0) left.push(name);
5807
+ else if (RIGHT_BUTTONS.indexOf(name) >= 0) right.push(name);
5808
+ else unknown.push(name);
5809
+ return {
5810
+ left,
5811
+ right,
5812
+ unknown
5813
+ };
5814
+ }
5815
+ //#endregion
5763
5816
  //#region src/yui_theme.js
5764
5817
  /***********************************************************************
5765
5818
  * yui_theme.js
@@ -5891,6 +5944,13 @@ var attrs_table$13 = [
5891
5944
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
5892
5945
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_rownum", 0, false, "Print first column with row number"),
5893
5946
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_delete_row_btn", 0, false, "Print last column with delete row button"),
5947
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "toolbar", 0, [
5948
+ "save",
5949
+ "undo",
5950
+ "clear",
5951
+ "copy",
5952
+ "paste"
5953
+ ], "Toolbar buttons to show, in order: save, undo, clear, copy, paste. [] hides the toolbar"),
5894
5954
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "tabulator_settings", 0, {
5895
5955
  layout: "fitDataFill",
5896
5956
  validationMode: "highlight",
@@ -6001,6 +6061,175 @@ async function readClipboard$1(gobj) {
6001
6061
  }
6002
6062
  }
6003
6063
  /************************************************************
6064
+ * Bottom toolbar, from the `toolbar` attr.
6065
+ *
6066
+ * The five buttons it can show, and their order, are the
6067
+ * caller's: a dialog with a single action asks for ["save"],
6068
+ * and [] leaves no toolbar at all. Default is the five it
6069
+ * always had, so nothing moves for a caller that says nothing.
6070
+ ************************************************************/
6071
+ function toolbar_button(gobj, name) {
6072
+ switch (name) {
6073
+ case "save": return [
6074
+ "button",
6075
+ {
6076
+ class: "button p-1 button-save",
6077
+ title: "save",
6078
+ "aria-label": "save",
6079
+ disabled: true
6080
+ },
6081
+ [[
6082
+ "span",
6083
+ { class: "icon m-0" },
6084
+ "<i class=\"yi-floppy-disk\"></i>"
6085
+ ], [
6086
+ "span",
6087
+ {
6088
+ class: "is-hidden-mobile pl-1 pr-1",
6089
+ i18n: "save"
6090
+ },
6091
+ "save"
6092
+ ]],
6093
+ { click: function(evt) {
6094
+ evt.stopPropagation();
6095
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SAVE_RECORD", {}, gobj);
6096
+ } }
6097
+ ];
6098
+ case "undo": return [
6099
+ "button",
6100
+ {
6101
+ class: "button p-1 button-undo",
6102
+ title: "undo",
6103
+ "aria-label": "undo",
6104
+ disabled: true
6105
+ },
6106
+ [[
6107
+ "span",
6108
+ { class: "icon m-0" },
6109
+ "<i class=\"yi-arrow-rotate-left\"></i>"
6110
+ ], [
6111
+ "span",
6112
+ {
6113
+ class: "is-hidden-mobile pl-1 pr-1",
6114
+ i18n: "undo"
6115
+ },
6116
+ "undo"
6117
+ ]],
6118
+ { click: function(evt) {
6119
+ evt.stopPropagation();
6120
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_UNDO_RECORD", {}, gobj);
6121
+ } }
6122
+ ];
6123
+ case "clear": return [
6124
+ "button",
6125
+ {
6126
+ class: "button p-1",
6127
+ title: "clear",
6128
+ "aria-label": "clear"
6129
+ },
6130
+ [[
6131
+ "span",
6132
+ { class: "icon m-0" },
6133
+ "<i class=\"yi-broom-wide\"></i>"
6134
+ ], [
6135
+ "span",
6136
+ {
6137
+ class: "is-hidden-mobile pl-1 pr-1",
6138
+ i18n: "clear"
6139
+ },
6140
+ "clear"
6141
+ ]],
6142
+ { click: function(evt) {
6143
+ evt.stopPropagation();
6144
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CLEAR_RECORD", {}, gobj);
6145
+ } }
6146
+ ];
6147
+ case "copy": return [
6148
+ "button",
6149
+ {
6150
+ class: "button p-1",
6151
+ title: "copy",
6152
+ "aria-label": "copy"
6153
+ },
6154
+ [[
6155
+ "span",
6156
+ { class: "icon m-0" },
6157
+ "<i class=\"yi-copy\"></i>"
6158
+ ], [
6159
+ "span",
6160
+ {
6161
+ class: "is-hidden-mobile pl-1 pr-1",
6162
+ i18n: "copy"
6163
+ },
6164
+ "copy"
6165
+ ]],
6166
+ { click: function(evt) {
6167
+ evt.stopPropagation();
6168
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_COPY_RECORD", {}, gobj);
6169
+ } }
6170
+ ];
6171
+ case "paste": return [
6172
+ "button",
6173
+ {
6174
+ class: "button p-1",
6175
+ title: "paste",
6176
+ "aria-label": "paste"
6177
+ },
6178
+ [[
6179
+ "span",
6180
+ { class: "icon m-0" },
6181
+ "<i class=\"yi-paste\"></i>"
6182
+ ], [
6183
+ "span",
6184
+ {
6185
+ class: "is-hidden-mobile pl-1 pr-1",
6186
+ i18n: "paste"
6187
+ },
6188
+ "paste"
6189
+ ]],
6190
+ { click: async function(evt) {
6191
+ evt.stopPropagation();
6192
+ await readClipboard$1(gobj);
6193
+ } }
6194
+ ];
6195
+ default: return null;
6196
+ }
6197
+ }
6198
+ function build_toolbar(gobj) {
6199
+ const plan = plan_toolbar((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "toolbar"));
6200
+ for (let name of plan.unknown) (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: unknown toolbar button '${name}'`);
6201
+ if (!plan.left.length && !plan.right.length) return [
6202
+ "span",
6203
+ {
6204
+ class: "yui-toolbar-form-empty",
6205
+ style: "display:none;"
6206
+ },
6207
+ ""
6208
+ ];
6209
+ return [
6210
+ "div",
6211
+ {
6212
+ class: "yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center",
6213
+ style: "width:100%; flex-wrap:wrap; row-gap:6px;"
6214
+ },
6215
+ [[
6216
+ "div",
6217
+ {
6218
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6219
+ style: "column-gap:6px;"
6220
+ },
6221
+ plan.left.map((n) => toolbar_button(gobj, n))
6222
+ ], [
6223
+ "div",
6224
+ {
6225
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6226
+ style: "column-gap:6px;"
6227
+ },
6228
+ plan.right.map((n) => toolbar_button(gobj, n))
6229
+ ]]
6230
+ ];
6231
+ }
6232
+ /************************************************************
6004
6233
  * Build UI
6005
6234
  ************************************************************/
6006
6235
  function build_ui$11(gobj) {
@@ -6017,149 +6246,7 @@ function build_ui$11(gobj) {
6017
6246
  style: "overflow-y:auto; min-height:0;"
6018
6247
  },
6019
6248
  build_form(gobj)
6020
- ], [
6021
- "div",
6022
- {
6023
- class: "yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center",
6024
- style: "width:100%; flex-wrap:wrap; row-gap:6px;"
6025
- },
6026
- [[
6027
- "div",
6028
- {
6029
- class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6030
- style: "column-gap:6px;"
6031
- },
6032
- [
6033
- [
6034
- "button",
6035
- {
6036
- class: "button p-1 button-save",
6037
- title: "save",
6038
- "aria-label": "save",
6039
- disabled: true
6040
- },
6041
- [[
6042
- "span",
6043
- { class: "icon m-0" },
6044
- "<i class=\"yi-floppy-disk\"></i>"
6045
- ], [
6046
- "span",
6047
- {
6048
- class: "is-hidden-mobile pl-1 pr-1",
6049
- i18n: "save"
6050
- },
6051
- "save"
6052
- ]],
6053
- { click: function(evt) {
6054
- evt.stopPropagation();
6055
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SAVE_RECORD", {}, gobj);
6056
- } }
6057
- ],
6058
- [
6059
- "button",
6060
- {
6061
- class: "button p-1 button-undo",
6062
- title: "undo",
6063
- "aria-label": "undo",
6064
- disabled: true
6065
- },
6066
- [[
6067
- "span",
6068
- { class: "icon m-0" },
6069
- "<i class=\"yi-arrow-rotate-left\"></i>"
6070
- ], [
6071
- "span",
6072
- {
6073
- class: "is-hidden-mobile pl-1 pr-1",
6074
- i18n: "undo"
6075
- },
6076
- "undo"
6077
- ]],
6078
- { click: function(evt) {
6079
- evt.stopPropagation();
6080
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_UNDO_RECORD", {}, gobj);
6081
- } }
6082
- ],
6083
- [
6084
- "button",
6085
- {
6086
- class: "button p-1",
6087
- title: "clear",
6088
- "aria-label": "clear"
6089
- },
6090
- [[
6091
- "span",
6092
- { class: "icon m-0" },
6093
- "<i class=\"yi-broom-wide\"></i>"
6094
- ], [
6095
- "span",
6096
- {
6097
- class: "is-hidden-mobile pl-1 pr-1",
6098
- i18n: "clear"
6099
- },
6100
- "clear"
6101
- ]],
6102
- { click: function(evt) {
6103
- evt.stopPropagation();
6104
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CLEAR_RECORD", {}, gobj);
6105
- } }
6106
- ]
6107
- ]
6108
- ], [
6109
- "div",
6110
- {
6111
- class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6112
- style: "column-gap:6px;"
6113
- },
6114
- [[
6115
- "button",
6116
- {
6117
- class: "button p-1",
6118
- title: "copy",
6119
- "aria-label": "copy"
6120
- },
6121
- [[
6122
- "span",
6123
- { class: "icon m-0" },
6124
- "<i class=\"yi-copy\"></i>"
6125
- ], [
6126
- "span",
6127
- {
6128
- class: "is-hidden-mobile pl-1 pr-1",
6129
- i18n: "copy"
6130
- },
6131
- "copy"
6132
- ]],
6133
- { click: function(evt) {
6134
- evt.stopPropagation();
6135
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_COPY_RECORD", {}, gobj);
6136
- } }
6137
- ], [
6138
- "button",
6139
- {
6140
- class: "button p-1",
6141
- title: "paste",
6142
- "aria-label": "paste"
6143
- },
6144
- [[
6145
- "span",
6146
- { class: "icon m-0" },
6147
- "<i class=\"yi-paste\"></i>"
6148
- ], [
6149
- "span",
6150
- {
6151
- class: "is-hidden-mobile pl-1 pr-1",
6152
- i18n: "paste"
6153
- },
6154
- "paste"
6155
- ]],
6156
- { click: async function(evt) {
6157
- evt.stopPropagation();
6158
- await readClipboard$1(gobj);
6159
- } }
6160
- ]]
6161
- ]]
6162
- ]]
6249
+ ], build_toolbar(gobj)]
6163
6250
  ]);
6164
6251
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", $container);
6165
6252
  apply_form_mode(gobj, $container.querySelector("form"));
@@ -885,7 +885,7 @@ function mt_start$15(gobj) {
885
885
  if (typeof shell_cfg.remember_section_position === "boolean") gobj_write_attr(gobj, "remember_section_position", shell_cfg.remember_section_position);
886
886
  build_item_index(gobj, config);
887
887
  instantiate_menus(gobj, config);
888
- build_toolbar(gobj, config);
888
+ build_toolbar$1(gobj, config);
889
889
  preinstantiate_eager_views(gobj);
890
890
  if (gobj_read_attr(gobj, "use_hash")) {
891
891
  priv.hash_handler = () => {
@@ -1611,7 +1611,7 @@ function safe_id(s) {
1611
1611
  * Per-item `show_on` is honoured: each rendered item is wrapped with
1612
1612
  * the same Bulma-helper logic as zones.
1613
1613
  ************************************************************/
1614
- function build_toolbar(gobj, config) {
1614
+ function build_toolbar$1(gobj, config) {
1615
1615
  let tb = config && config.toolbar;
1616
1616
  if (!tb || !is_array(tb.items)) return;
1617
1617
  let priv = gobj.priv;
@@ -5755,6 +5755,59 @@ function refresh_clear($input) {
5755
5755
  $btn.classList.toggle("is-visible", clear_visible($input));
5756
5756
  }
5757
5757
  //#endregion
5758
+ //#region src/form_toolbar_plan.js
5759
+ /***********************************************************************
5760
+ * form_toolbar_plan.js
5761
+ *
5762
+ * Which buttons the C_YUI_FORM toolbar shows, and where.
5763
+ *
5764
+ * Pure, so it can be tested without a DOM: the gclass turns the
5765
+ * plan into elements, this decides the plan.
5766
+ *
5767
+ * Copyright (c) 2026, ArtGins.
5768
+ * All Rights Reserved.
5769
+ ***********************************************************************/
5770
+ var LEFT_BUTTONS = [
5771
+ "save",
5772
+ "undo",
5773
+ "clear"
5774
+ ];
5775
+ var RIGHT_BUTTONS = ["copy", "paste"];
5776
+ var DEFAULT_TOOLBAR = [
5777
+ "save",
5778
+ "undo",
5779
+ "clear",
5780
+ "copy",
5781
+ "paste"
5782
+ ];
5783
+ /***************************************************************
5784
+ * plan_toolbar(wanted) -> {left, right, unknown}
5785
+ *
5786
+ * wanted an array of button names, in the order asked for.
5787
+ * Anything that is not an array means "the default
5788
+ * five", so a caller that sets nothing keeps exactly
5789
+ * the toolbar this gclass always had.
5790
+ * [] means no toolbar at all.
5791
+ *
5792
+ * unknown names that match no button. They are reported and
5793
+ * NOT silently dropped: a typo in the list would
5794
+ * otherwise remove the save button with no trace.
5795
+ ***************************************************************/
5796
+ function plan_toolbar(wanted) {
5797
+ if (!Array.isArray(wanted)) wanted = DEFAULT_TOOLBAR;
5798
+ let left = [];
5799
+ let right = [];
5800
+ let unknown = [];
5801
+ for (let name of wanted) if (LEFT_BUTTONS.indexOf(name) >= 0) left.push(name);
5802
+ else if (RIGHT_BUTTONS.indexOf(name) >= 0) right.push(name);
5803
+ else unknown.push(name);
5804
+ return {
5805
+ left,
5806
+ right,
5807
+ unknown
5808
+ };
5809
+ }
5810
+ //#endregion
5758
5811
  //#region src/yui_theme.js
5759
5812
  /***********************************************************************
5760
5813
  * yui_theme.js
@@ -5886,6 +5939,13 @@ var attrs_table$13 = [
5886
5939
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
5887
5940
  SDATA(data_type_t.DTP_BOOLEAN, "with_rownum", 0, false, "Print first column with row number"),
5888
5941
  SDATA(data_type_t.DTP_BOOLEAN, "with_delete_row_btn", 0, false, "Print last column with delete row button"),
5942
+ SDATA(data_type_t.DTP_JSON, "toolbar", 0, [
5943
+ "save",
5944
+ "undo",
5945
+ "clear",
5946
+ "copy",
5947
+ "paste"
5948
+ ], "Toolbar buttons to show, in order: save, undo, clear, copy, paste. [] hides the toolbar"),
5889
5949
  SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
5890
5950
  layout: "fitDataFill",
5891
5951
  validationMode: "highlight",
@@ -5996,6 +6056,175 @@ async function readClipboard$1(gobj) {
5996
6056
  }
5997
6057
  }
5998
6058
  /************************************************************
6059
+ * Bottom toolbar, from the `toolbar` attr.
6060
+ *
6061
+ * The five buttons it can show, and their order, are the
6062
+ * caller's: a dialog with a single action asks for ["save"],
6063
+ * and [] leaves no toolbar at all. Default is the five it
6064
+ * always had, so nothing moves for a caller that says nothing.
6065
+ ************************************************************/
6066
+ function toolbar_button(gobj, name) {
6067
+ switch (name) {
6068
+ case "save": return [
6069
+ "button",
6070
+ {
6071
+ class: "button p-1 button-save",
6072
+ title: "save",
6073
+ "aria-label": "save",
6074
+ disabled: true
6075
+ },
6076
+ [[
6077
+ "span",
6078
+ { class: "icon m-0" },
6079
+ "<i class=\"yi-floppy-disk\"></i>"
6080
+ ], [
6081
+ "span",
6082
+ {
6083
+ class: "is-hidden-mobile pl-1 pr-1",
6084
+ i18n: "save"
6085
+ },
6086
+ "save"
6087
+ ]],
6088
+ { click: function(evt) {
6089
+ evt.stopPropagation();
6090
+ gobj_send_event(gobj, "EV_SAVE_RECORD", {}, gobj);
6091
+ } }
6092
+ ];
6093
+ case "undo": return [
6094
+ "button",
6095
+ {
6096
+ class: "button p-1 button-undo",
6097
+ title: "undo",
6098
+ "aria-label": "undo",
6099
+ disabled: true
6100
+ },
6101
+ [[
6102
+ "span",
6103
+ { class: "icon m-0" },
6104
+ "<i class=\"yi-arrow-rotate-left\"></i>"
6105
+ ], [
6106
+ "span",
6107
+ {
6108
+ class: "is-hidden-mobile pl-1 pr-1",
6109
+ i18n: "undo"
6110
+ },
6111
+ "undo"
6112
+ ]],
6113
+ { click: function(evt) {
6114
+ evt.stopPropagation();
6115
+ gobj_send_event(gobj, "EV_UNDO_RECORD", {}, gobj);
6116
+ } }
6117
+ ];
6118
+ case "clear": return [
6119
+ "button",
6120
+ {
6121
+ class: "button p-1",
6122
+ title: "clear",
6123
+ "aria-label": "clear"
6124
+ },
6125
+ [[
6126
+ "span",
6127
+ { class: "icon m-0" },
6128
+ "<i class=\"yi-broom-wide\"></i>"
6129
+ ], [
6130
+ "span",
6131
+ {
6132
+ class: "is-hidden-mobile pl-1 pr-1",
6133
+ i18n: "clear"
6134
+ },
6135
+ "clear"
6136
+ ]],
6137
+ { click: function(evt) {
6138
+ evt.stopPropagation();
6139
+ gobj_send_event(gobj, "EV_CLEAR_RECORD", {}, gobj);
6140
+ } }
6141
+ ];
6142
+ case "copy": return [
6143
+ "button",
6144
+ {
6145
+ class: "button p-1",
6146
+ title: "copy",
6147
+ "aria-label": "copy"
6148
+ },
6149
+ [[
6150
+ "span",
6151
+ { class: "icon m-0" },
6152
+ "<i class=\"yi-copy\"></i>"
6153
+ ], [
6154
+ "span",
6155
+ {
6156
+ class: "is-hidden-mobile pl-1 pr-1",
6157
+ i18n: "copy"
6158
+ },
6159
+ "copy"
6160
+ ]],
6161
+ { click: function(evt) {
6162
+ evt.stopPropagation();
6163
+ gobj_send_event(gobj, "EV_COPY_RECORD", {}, gobj);
6164
+ } }
6165
+ ];
6166
+ case "paste": return [
6167
+ "button",
6168
+ {
6169
+ class: "button p-1",
6170
+ title: "paste",
6171
+ "aria-label": "paste"
6172
+ },
6173
+ [[
6174
+ "span",
6175
+ { class: "icon m-0" },
6176
+ "<i class=\"yi-paste\"></i>"
6177
+ ], [
6178
+ "span",
6179
+ {
6180
+ class: "is-hidden-mobile pl-1 pr-1",
6181
+ i18n: "paste"
6182
+ },
6183
+ "paste"
6184
+ ]],
6185
+ { click: async function(evt) {
6186
+ evt.stopPropagation();
6187
+ await readClipboard$1(gobj);
6188
+ } }
6189
+ ];
6190
+ default: return null;
6191
+ }
6192
+ }
6193
+ function build_toolbar(gobj) {
6194
+ const plan = plan_toolbar(gobj_read_attr(gobj, "toolbar"));
6195
+ for (let name of plan.unknown) log_warning(`${gobj_short_name(gobj)}: unknown toolbar button '${name}'`);
6196
+ if (!plan.left.length && !plan.right.length) return [
6197
+ "span",
6198
+ {
6199
+ class: "yui-toolbar-form-empty",
6200
+ style: "display:none;"
6201
+ },
6202
+ ""
6203
+ ];
6204
+ return [
6205
+ "div",
6206
+ {
6207
+ class: "yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center",
6208
+ style: "width:100%; flex-wrap:wrap; row-gap:6px;"
6209
+ },
6210
+ [[
6211
+ "div",
6212
+ {
6213
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6214
+ style: "column-gap:6px;"
6215
+ },
6216
+ plan.left.map((n) => toolbar_button(gobj, n))
6217
+ ], [
6218
+ "div",
6219
+ {
6220
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6221
+ style: "column-gap:6px;"
6222
+ },
6223
+ plan.right.map((n) => toolbar_button(gobj, n))
6224
+ ]]
6225
+ ];
6226
+ }
6227
+ /************************************************************
5999
6228
  * Build UI
6000
6229
  ************************************************************/
6001
6230
  function build_ui$11(gobj) {
@@ -6012,149 +6241,7 @@ function build_ui$11(gobj) {
6012
6241
  style: "overflow-y:auto; min-height:0;"
6013
6242
  },
6014
6243
  build_form(gobj)
6015
- ], [
6016
- "div",
6017
- {
6018
- class: "yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center",
6019
- style: "width:100%; flex-wrap:wrap; row-gap:6px;"
6020
- },
6021
- [[
6022
- "div",
6023
- {
6024
- class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6025
- style: "column-gap:6px;"
6026
- },
6027
- [
6028
- [
6029
- "button",
6030
- {
6031
- class: "button p-1 button-save",
6032
- title: "save",
6033
- "aria-label": "save",
6034
- disabled: true
6035
- },
6036
- [[
6037
- "span",
6038
- { class: "icon m-0" },
6039
- "<i class=\"yi-floppy-disk\"></i>"
6040
- ], [
6041
- "span",
6042
- {
6043
- class: "is-hidden-mobile pl-1 pr-1",
6044
- i18n: "save"
6045
- },
6046
- "save"
6047
- ]],
6048
- { click: function(evt) {
6049
- evt.stopPropagation();
6050
- gobj_send_event(gobj, "EV_SAVE_RECORD", {}, gobj);
6051
- } }
6052
- ],
6053
- [
6054
- "button",
6055
- {
6056
- class: "button p-1 button-undo",
6057
- title: "undo",
6058
- "aria-label": "undo",
6059
- disabled: true
6060
- },
6061
- [[
6062
- "span",
6063
- { class: "icon m-0" },
6064
- "<i class=\"yi-arrow-rotate-left\"></i>"
6065
- ], [
6066
- "span",
6067
- {
6068
- class: "is-hidden-mobile pl-1 pr-1",
6069
- i18n: "undo"
6070
- },
6071
- "undo"
6072
- ]],
6073
- { click: function(evt) {
6074
- evt.stopPropagation();
6075
- gobj_send_event(gobj, "EV_UNDO_RECORD", {}, gobj);
6076
- } }
6077
- ],
6078
- [
6079
- "button",
6080
- {
6081
- class: "button p-1",
6082
- title: "clear",
6083
- "aria-label": "clear"
6084
- },
6085
- [[
6086
- "span",
6087
- { class: "icon m-0" },
6088
- "<i class=\"yi-broom-wide\"></i>"
6089
- ], [
6090
- "span",
6091
- {
6092
- class: "is-hidden-mobile pl-1 pr-1",
6093
- i18n: "clear"
6094
- },
6095
- "clear"
6096
- ]],
6097
- { click: function(evt) {
6098
- evt.stopPropagation();
6099
- gobj_send_event(gobj, "EV_CLEAR_RECORD", {}, gobj);
6100
- } }
6101
- ]
6102
- ]
6103
- ], [
6104
- "div",
6105
- {
6106
- class: "p-1 is-flex is-flex-direction-row is-align-items-center",
6107
- style: "column-gap:6px;"
6108
- },
6109
- [[
6110
- "button",
6111
- {
6112
- class: "button p-1",
6113
- title: "copy",
6114
- "aria-label": "copy"
6115
- },
6116
- [[
6117
- "span",
6118
- { class: "icon m-0" },
6119
- "<i class=\"yi-copy\"></i>"
6120
- ], [
6121
- "span",
6122
- {
6123
- class: "is-hidden-mobile pl-1 pr-1",
6124
- i18n: "copy"
6125
- },
6126
- "copy"
6127
- ]],
6128
- { click: function(evt) {
6129
- evt.stopPropagation();
6130
- gobj_send_event(gobj, "EV_COPY_RECORD", {}, gobj);
6131
- } }
6132
- ], [
6133
- "button",
6134
- {
6135
- class: "button p-1",
6136
- title: "paste",
6137
- "aria-label": "paste"
6138
- },
6139
- [[
6140
- "span",
6141
- { class: "icon m-0" },
6142
- "<i class=\"yi-paste\"></i>"
6143
- ], [
6144
- "span",
6145
- {
6146
- class: "is-hidden-mobile pl-1 pr-1",
6147
- i18n: "paste"
6148
- },
6149
- "paste"
6150
- ]],
6151
- { click: async function(evt) {
6152
- evt.stopPropagation();
6153
- await readClipboard$1(gobj);
6154
- } }
6155
- ]]
6156
- ]]
6157
- ]]
6244
+ ], build_toolbar(gobj)]
6158
6245
  ]);
6159
6246
  gobj_write_attr(gobj, "$container", $container);
6160
6247
  apply_form_mode(gobj, $container.querySelector("form"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -38,7 +38,7 @@
38
38
  "@yuneta/gobj-js": "^7.8.7",
39
39
  "bulma": "^1.0.4",
40
40
  "i18next": "^26.3.6",
41
- "maplibre-gl": "^6.0.0",
41
+ "maplibre-gl": "^6.1.0",
42
42
  "tabulator-tables": "^6.5.2",
43
43
  "tom-select": "^2.6.2",
44
44
  "uplot": "^1.6.32",
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
- ['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;'}, [
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
 
@@ -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
+ });