@yuneta/gobj-ui 7.11.1 → 7.13.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 CHANGED
@@ -1031,6 +1031,23 @@ from the view's `EV_LANGUAGE_CHANGED` action.
1031
1031
  table that is not built yet or is already gone. Clear the selection after
1032
1032
  acting on it: the rows it names are no longer there.
1033
1033
 
1034
+ **In the treedb topic table** (`C_YUI_TREEDB_TOPIC_WITH_FORM`, and through it
1035
+ `C_YUI_TREEDB_TOPICS`) the bar is **opt-in**: pass `with_selection_bar: true`.
1036
+ It is off by default because the bar takes its words from the HOST's i18n, and
1037
+ a host that has not defined `"{{n}} selected"` and `"clear selection"` renders
1038
+ the keys. It shows only while the table is in **edition mode** — outside it the
1039
+ checkbox column is hidden, and a count of rows nobody can see or untick is a
1040
+ count you cannot act on. The bar carries no action there: the table's own
1041
+ toolbar already has Delete and Copy, and they act on the selection.
1042
+
1043
+ `yui_selection_column({visible: false})` is for a table that reveals the column
1044
+ only in an edit mode — `C_YUI_TREEDB_TOPIC_WITH_FORM` shows it with
1045
+ `showColumn("_check_box_state_")`, and takes its `selectableRows` and every
1046
+ read of its selection from here too. A **radio** column (pick ONE row) is not
1047
+ this facility: it is `formatter: "rowSelection"` with no `titleFormatter` and
1048
+ `selectableRows: 1`, and the header checkbox, the count and the bar all mean
1049
+ nothing there.
1050
+
1034
1051
  ## Conventions
1035
1052
 
1036
1053
  ### i18n: a string must be able to CHANGE language, not just be translated once
@@ -14496,6 +14496,7 @@ var attrs_table$6 = [
14496
14496
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
14497
14497
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
14498
14498
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. Safe against a backend that cannot page: it answers the whole list, which the table reads as one page"),
14499
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Each topic table shows the shared selection bar (\"N selected\" + a way out) above it while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, so a host that turns it on must define \"{{n}} selected\" and \"clear selection\""),
14499
14500
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
14500
14501
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
14501
14502
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_cards_landing", 0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
@@ -15310,7 +15311,8 @@ function process_treedb_descs$1(gobj) {
15310
15311
  treedb_name,
15311
15312
  topic_name: key,
15312
15313
  desc,
15313
- with_remote_paging: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging")
15314
+ with_remote_paging: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging"),
15315
+ with_selection_bar: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_selection_bar")
15314
15316
  };
15315
15317
  let id = `${(0, _yuneta_gobj_js.gobj_name)(gobj)}?${desc.topic_name}`;
15316
15318
  let gobj_topic_form = (0, _yuneta_gobj_js.gobj_create_service)(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
@@ -16317,6 +16319,206 @@ function yui_tabulator_relocalize(table, t) {
16317
16319
  }
16318
16320
  }
16319
16321
  //#endregion
16322
+ //#region src/yui_table_select.js
16323
+ /***********************************************************************
16324
+ * yui_table_select.js
16325
+ *
16326
+ * SELECT ROWS IN A TABLE, and act on the lot.
16327
+ *
16328
+ * Deleting twenty rows one confirmation at a time is not a
16329
+ * workflow, it is a punishment, and every table that lets you
16330
+ * remove a row will eventually be asked to remove twenty. This
16331
+ * is that facility, once: the checkbox column, the settings
16332
+ * that make selection behave, and the bar that appears while
16333
+ * something is selected and carries what can be done to it.
16334
+ *
16335
+ * Two decisions are baked in, both learned in the treedb
16336
+ * topic table:
16337
+ *
16338
+ * - Selection is driven ONLY by the checkbox, never by
16339
+ * clicking the row (`selectableRows: "highlight"`). A table
16340
+ * row is full of things to click -- an editor, an icon, a
16341
+ * nested table -- and click-to-select ticks a row every time
16342
+ * you reach for one of them.
16343
+ *
16344
+ * - The header checkbox covers the ACTIVE rows, the ones the
16345
+ * filters leave on screen. "Select all" over rows nobody can
16346
+ * see is how a filtered delete takes the whole topic with
16347
+ * it.
16348
+ *
16349
+ * The bar takes its words from the HOST's `t`: this library
16350
+ * translates through the app's i18next, and the app owns the
16351
+ * keys. It needs "{{n}} selected" and "clear selection"; each
16352
+ * action brings its own key.
16353
+ *
16354
+ * Copyright (c) 2026, ArtGins.
16355
+ * All Rights Reserved.
16356
+ ***********************************************************************/
16357
+ /***************************************************************
16358
+ * The checkbox column. Put it FIRST in the column list.
16359
+ *
16360
+ * opts:
16361
+ * field the (virtual) field name, default "_select"
16362
+ * width default 44
16363
+ * visible default true; `false` for a table that reveals
16364
+ * the column only in an edit mode (the treedb topic
16365
+ * table shows it with `showColumn(field)`)
16366
+ *
16367
+ * This is the SELECT-MANY widget. A table that lets you pick ONE
16368
+ * row uses a radio column of its own (`formatter: "rowSelection"`
16369
+ * with no `titleFormatter`, `selectableRows: 1`): the header
16370
+ * checkbox, the count and the bar all mean nothing there.
16371
+ ***************************************************************/
16372
+ function yui_selection_column(opts) {
16373
+ let o = opts || {};
16374
+ return {
16375
+ title: "",
16376
+ field: o.field || "_select",
16377
+ width: o.width || 44,
16378
+ minWidth: o.width || 44,
16379
+ visible: o.visible !== false,
16380
+ hozAlign: "center",
16381
+ headerHozAlign: "center",
16382
+ headerSort: false,
16383
+ resizable: false,
16384
+ formatter: "rowSelection",
16385
+ titleFormatter: "rowSelection",
16386
+ titleFormatterParams: { rowRange: "active" },
16387
+ cssClass: "TABLE_SELECT_CELL"
16388
+ };
16389
+ }
16390
+ /***************************************************************
16391
+ * What the Tabulator settings need for the column above.
16392
+ * Spread it into the settings object.
16393
+ ***************************************************************/
16394
+ function yui_selection_settings() {
16395
+ return { selectableRows: "highlight" };
16396
+ }
16397
+ /***************************************************************
16398
+ * The rows selected right now (never null).
16399
+ ***************************************************************/
16400
+ function yui_selected_rows(tabulator) {
16401
+ if (!tabulator || typeof tabulator.getSelectedData !== "function") return [];
16402
+ try {
16403
+ return tabulator.getSelectedData() || [];
16404
+ } catch (e) {
16405
+ return [];
16406
+ }
16407
+ }
16408
+ /***************************************************************
16409
+ * Drop the selection (after acting on it, or after a
16410
+ * reload that leaves the ticked rows behind).
16411
+ ***************************************************************/
16412
+ function yui_clear_selection(tabulator) {
16413
+ if (!tabulator || typeof tabulator.deselectRow !== "function") return;
16414
+ try {
16415
+ tabulator.deselectRow();
16416
+ } catch (e) {}
16417
+ }
16418
+ /***************************************************************
16419
+ * The bar that appears while rows are selected.
16420
+ *
16421
+ * t the HOST's translator
16422
+ * opts:
16423
+ * actions [{label, icon, class, on_click}] -- `label` is an
16424
+ * i18n KEY, `class` extra Bulma classes for the
16425
+ * button (e.g. "is-danger"), `on_click` the DOM
16426
+ * handler, whose job is to send an event
16427
+ * on_clear called by the "clear selection" button
16428
+ * name logical-class prefix, default "TABLE"
16429
+ *
16430
+ * -> {$el, set_count(n), refresh()}
16431
+ *
16432
+ * `refresh()` redraws it in the current language: the count is
16433
+ * composed at render time, so it cannot re-translate itself
16434
+ * through `refresh_language()`. Call it on EV_LANGUAGE_CHANGED.
16435
+ ***************************************************************/
16436
+ function yui_selection_bar(t, opts) {
16437
+ let o = opts || {};
16438
+ let actions = Array.isArray(o.actions) ? o.actions : [];
16439
+ let name = o.name || "TABLE";
16440
+ let count = 0;
16441
+ let $count = (0, _yuneta_gobj_js.createElement2)([
16442
+ "span",
16443
+ { class: `${name}_SELECTION_COUNT has-text-weight-semibold` },
16444
+ ""
16445
+ ]);
16446
+ let $buttons = [];
16447
+ for (let action of actions) {
16448
+ let $b = (0, _yuneta_gobj_js.createElement2)([
16449
+ "button",
16450
+ {
16451
+ class: `${name}_SELECTION_ACTION button ${action.class || ""}`,
16452
+ type: "button"
16453
+ },
16454
+ [[
16455
+ "span",
16456
+ { class: "icon" },
16457
+ [["i", { class: action.icon || "yi-check" }]]
16458
+ ], [
16459
+ "span",
16460
+ {},
16461
+ ""
16462
+ ]],
16463
+ { click: action.on_click }
16464
+ ]);
16465
+ $b._yui_label = action.label || "";
16466
+ $buttons.push($b);
16467
+ }
16468
+ let $clear = (0, _yuneta_gobj_js.createElement2)([
16469
+ "button",
16470
+ {
16471
+ class: `${name}_SELECTION_CLEAR button is-ghost`,
16472
+ type: "button"
16473
+ },
16474
+ [[
16475
+ "span",
16476
+ { class: "icon" },
16477
+ [["i", { class: "yi-xmark" }]]
16478
+ ], [
16479
+ "span",
16480
+ {},
16481
+ ""
16482
+ ]],
16483
+ { click: function(e) {
16484
+ if (typeof o.on_clear === "function") o.on_clear(e);
16485
+ } }
16486
+ ]);
16487
+ $clear._yui_label = "clear selection";
16488
+ let $el = (0, _yuneta_gobj_js.createElement2)([
16489
+ "div",
16490
+ {
16491
+ class: `${name}_SELECTION_BAR is-align-items-center is-flex-wrap-wrap mb-2 is-hidden`,
16492
+ style: "display:flex; gap:0.5rem;"
16493
+ },
16494
+ [$count].concat($buttons, [$clear])
16495
+ ]);
16496
+ function label_of($b) {
16497
+ return $b.querySelector("span.icon ~ span");
16498
+ }
16499
+ function refresh() {
16500
+ $count.textContent = t("{{n}} selected", { n: count });
16501
+ for (let $b of $buttons.concat([$clear])) {
16502
+ let $label = label_of($b);
16503
+ let key = $b._yui_label;
16504
+ if ($label && key) $label.textContent = t(key);
16505
+ $b.title = key ? t(key) : "";
16506
+ $b.setAttribute("aria-label", $b.title);
16507
+ }
16508
+ $el.classList.toggle("is-hidden", count <= 0);
16509
+ }
16510
+ function set_count(n) {
16511
+ count = n > 0 ? n : 0;
16512
+ refresh();
16513
+ }
16514
+ refresh();
16515
+ return {
16516
+ $el,
16517
+ set_count,
16518
+ refresh
16519
+ };
16520
+ }
16521
+ //#endregion
16320
16522
  //#region src/treedb_write_plan.js
16321
16523
  /***********************************************************************
16322
16524
  * treedb_write_plan.js
@@ -16484,6 +16686,7 @@ var attrs_table$5 = [
16484
16686
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16485
16687
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
16486
16688
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_radio", 0, false, "Auxiliary first column to select one row"),
16689
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Show the shared selection bar (\"N selected\" + a way out) above the table while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, and a host that has not defined \"{{n}} selected\" and \"clear selection\" would render the keys"),
16487
16690
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "Broadcast select rows event"),
16488
16691
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "Broadcast unselect rows event"),
16489
16692
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "tabulator_settings", 0, {
@@ -16502,6 +16705,7 @@ var attrs_table$5 = [
16502
16705
  }, "Default settings for Tabulator"),
16503
16706
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "HTML container for UI"),
16504
16707
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "tabulator", 0, null, "Tabulator instance"),
16708
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "selection_bar", 0, null, "The selection bar (yui_table_select.js), when with_selection_bar"),
16505
16709
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "table_id", 0, null, "Table div ID"),
16506
16710
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "toolbar_id", 0, null, "Toolbar ID"),
16507
16711
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "popup_id", 0, null, "Edit form popup ID"),
@@ -16918,6 +17122,15 @@ function build_ui$5(gobj) {
16918
17122
  style: "margin-top:0px !important;"
16919
17123
  }]]
16920
17124
  ]);
17125
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_checkbox") && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_selection_bar")) {
17126
+ let bar = yui_selection_bar(i18next.t, {
17127
+ name: "TREEDB_TABLE",
17128
+ actions: [],
17129
+ on_clear: () => (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CLEAR_SELECTION", {}, gobj)
17130
+ });
17131
+ (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "selection_bar", bar);
17132
+ $container.insertBefore(bar.$el, $container.querySelector(`#${table_id}`));
17133
+ }
16921
17134
  let $toolbar_slot = $container.querySelector(".toolbar_tabulator_table");
16922
17135
  if ($table_toolbar instanceof Element) $toolbar_slot.appendChild($table_toolbar);
16923
17136
  else $toolbar_slot.appendChild((0, _yuneta_gobj_js.createElement2)(["div", {}]));
@@ -16926,6 +17139,24 @@ function build_ui$5(gobj) {
16926
17139
  (0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
16927
17140
  }
16928
17141
  /************************************************************
17142
+ * How many rows are ticked, on the bar.
17143
+ *
17144
+ * Only while the table is EDITABLE: outside edition its checkbox
17145
+ * column is hidden (`hideColumn`), and a count of rows nobody can
17146
+ * see or untick is a count you cannot act on. The selection itself
17147
+ * is left alone -- coming back to edition finds it, and the bar
17148
+ * recounts from the table.
17149
+ ************************************************************/
17150
+ function render_selection_bar(gobj) {
17151
+ let bar = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "selection_bar");
17152
+ if (!bar) return;
17153
+ if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable")) {
17154
+ bar.set_count(0);
17155
+ return;
17156
+ }
17157
+ bar.set_count(yui_selected_rows((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator")).length);
17158
+ }
17159
+ /************************************************************
16929
17160
  * Destroy UI
16930
17161
  ************************************************************/
16931
17162
  function destroy_ui$2(gobj) {
@@ -16996,16 +17227,11 @@ function create_tabulator(gobj) {
16996
17227
  let columns = [];
16997
17228
  let with_checkbox = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_checkbox");
16998
17229
  let with_radio = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_radio");
16999
- if (with_checkbox) columns.push({
17000
- formatter: "rowSelection",
17001
- titleFormatter: "rowSelection",
17230
+ if (with_checkbox) columns.push(yui_selection_column({
17002
17231
  field: "_check_box_state_",
17003
- hozAlign: "center",
17004
- headerHozAlign: "center",
17005
17232
  width: 40,
17006
- visible: false,
17007
- headerSort: false
17008
- });
17233
+ visible: false
17234
+ }));
17009
17235
  else if (with_radio) columns.push({
17010
17236
  formatter: "rowSelection",
17011
17237
  field: "_check_box_state_",
@@ -17234,7 +17460,9 @@ function create_tabulator(gobj) {
17234
17460
  }
17235
17461
  });
17236
17462
  let pkey = desc.pkey || "id";
17237
- let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
17463
+ let selectable = false;
17464
+ if (with_checkbox) selectable = yui_selection_settings().selectableRows;
17465
+ else if (with_radio) selectable = 1;
17238
17466
  let tabulator_settings = (0, _yuneta_gobj_js.json_deep_copy)((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator_settings"));
17239
17467
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging")) {
17240
17468
  let page_size = (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "page_size") || 200;
@@ -18118,6 +18346,8 @@ function refuse_if_readonly$2(gobj, event) {
18118
18346
  * dict. Re-apply it in the new language.
18119
18347
  ***************************************************************/
18120
18348
  function ac_language_changed$2(gobj, event, kw, src) {
18349
+ let bar = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "selection_bar");
18350
+ if (bar) bar.refresh();
18121
18351
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18122
18352
  if (!tabulator) return 0;
18123
18353
  yui_tabulator_relocalize(tabulator, i18next.t);
@@ -18216,7 +18446,7 @@ function ac_edition_mode(gobj, event, kw, src) {
18216
18446
  tabulator.showColumn("_check_box_state_");
18217
18447
  $button_new_record.removeAttribute("disabled");
18218
18448
  $button_paste_record.removeAttribute("disabled");
18219
- if (tabulator.getSelectedData().length) {
18449
+ if (yui_selected_rows(tabulator).length) {
18220
18450
  $button_delete_record.removeAttribute("disabled");
18221
18451
  $button_copy_record.removeAttribute("disabled");
18222
18452
  }
@@ -18231,6 +18461,7 @@ function ac_edition_mode(gobj, event, kw, src) {
18231
18461
  $button_copy_record.setAttribute("disabled", true);
18232
18462
  $button_paste_record.setAttribute("disabled", true);
18233
18463
  }
18464
+ render_selection_bar(gobj);
18234
18465
  return 0;
18235
18466
  }
18236
18467
  /************************************************************
@@ -18274,7 +18505,7 @@ function ac_delete_rows(gobj, event, kw, src) {
18274
18505
  if (refuse_if_readonly$2(gobj, event)) return -1;
18275
18506
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18276
18507
  if ((0, _yuneta_gobj_js.json_size)(kw) === 0) {
18277
- let rows = tabulator.getSelectedData();
18508
+ let rows = yui_selected_rows(tabulator);
18278
18509
  if (!rows.length) {
18279
18510
  yui_shell_confirm_ok(yui_shell_of(gobj), "please select some row", {
18280
18511
  t: i18next.t,
@@ -18310,7 +18541,7 @@ function ac_delete_rows(gobj, event, kw, src) {
18310
18541
  * From internal
18311
18542
  ************************************************************/
18312
18543
  function ac_copy_rows(gobj, event, kw, src) {
18313
- let rows = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator").getSelectedData();
18544
+ let rows = yui_selected_rows((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator"));
18314
18545
  if (!rows.length) {
18315
18546
  yui_shell_confirm_ok(yui_shell_of(gobj), "please select some row", {
18316
18547
  t: i18next.t,
@@ -18368,7 +18599,7 @@ function ac_select_rows(gobj, event, kw, src) {
18368
18599
  let $button_delete_record = $container.querySelector(`.button-delete-record`);
18369
18600
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
18370
18601
  if ($button_delete_record) {
18371
- if (tabulator.getSelectedData().length && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable")) {
18602
+ if (yui_selected_rows(tabulator).length && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable")) {
18372
18603
  $button_delete_record.removeAttribute("disabled");
18373
18604
  $button_copy_record.removeAttribute("disabled");
18374
18605
  } else {
@@ -18376,6 +18607,7 @@ function ac_select_rows(gobj, event, kw, src) {
18376
18607
  $button_copy_record.setAttribute("disabled", true);
18377
18608
  }
18378
18609
  }
18610
+ render_selection_bar(gobj);
18379
18611
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "broadcast_select_rows_event")) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, event, kw);
18380
18612
  return 0;
18381
18613
  }
@@ -18388,7 +18620,7 @@ function ac_unselect_rows(gobj, event, kw, src) {
18388
18620
  let $button_delete_record = $container.querySelector(`.button-delete-record`);
18389
18621
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
18390
18622
  if ($button_delete_record) {
18391
- if (tabulator.getSelectedData().length && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable")) {
18623
+ if (yui_selected_rows(tabulator).length && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable")) {
18392
18624
  $button_delete_record.removeAttribute("disabled");
18393
18625
  $button_copy_record.removeAttribute("disabled");
18394
18626
  } else {
@@ -18396,10 +18628,19 @@ function ac_unselect_rows(gobj, event, kw, src) {
18396
18628
  $button_copy_record.setAttribute("disabled", true);
18397
18629
  }
18398
18630
  }
18631
+ render_selection_bar(gobj);
18399
18632
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "broadcast_select_rows_event")) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, event, kw);
18400
18633
  return 0;
18401
18634
  }
18402
18635
  /************************************************************
18636
+ * The way out of a selection (the bar's clear button).
18637
+ ************************************************************/
18638
+ function ac_clear_selection(gobj, event, kw, src) {
18639
+ yui_clear_selection((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator"));
18640
+ render_selection_bar(gobj);
18641
+ return 0;
18642
+ }
18643
+ /************************************************************
18403
18644
  * { locale: "es" }
18404
18645
  ************************************************************/
18405
18646
  function ac_change_locale(gobj, event, kw, src) {
@@ -18876,6 +19117,11 @@ function create_gclass$5(gclass_name) {
18876
19117
  ac_select_rows,
18877
19118
  null
18878
19119
  ],
19120
+ [
19121
+ "EV_CLEAR_SELECTION",
19122
+ ac_clear_selection,
19123
+ null
19124
+ ],
18879
19125
  [
18880
19126
  "EV_UNSELECT_ROWS",
18881
19127
  ac_unselect_rows,
@@ -18984,6 +19230,7 @@ function create_gclass$5(gclass_name) {
18984
19230
  ["EV_DELETE_ROWS", 0],
18985
19231
  ["EV_COPY_ROWS", 0],
18986
19232
  ["EV_PASTE_ROWS", 0],
19233
+ ["EV_CLEAR_SELECTION", 0],
18987
19234
  ["EV_SELECT_ROWS", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18988
19235
  ["EV_UNSELECT_ROWS", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18989
19236
  ["EV_SHOW_HOOK_DATA", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
@@ -14491,6 +14491,7 @@ var attrs_table$6 = [
14491
14491
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
14492
14492
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
14493
14493
  SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. Safe against a backend that cannot page: it answers the whole list, which the table reads as one page"),
14494
+ SDATA(data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Each topic table shows the shared selection bar (\"N selected\" + a way out) above it while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, so a host that turns it on must define \"{{n}} selected\" and \"clear selection\""),
14494
14495
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
14495
14496
  SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
14496
14497
  SDATA(data_type_t.DTP_BOOLEAN, "with_cards_landing", 0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
@@ -15305,7 +15306,8 @@ function process_treedb_descs$1(gobj) {
15305
15306
  treedb_name,
15306
15307
  topic_name: key,
15307
15308
  desc,
15308
- with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging")
15309
+ with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging"),
15310
+ with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar")
15309
15311
  };
15310
15312
  let id = `${gobj_name(gobj)}?${desc.topic_name}`;
15311
15313
  let gobj_topic_form = gobj_create_service(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
@@ -16312,6 +16314,206 @@ function yui_tabulator_relocalize(table, t) {
16312
16314
  }
16313
16315
  }
16314
16316
  //#endregion
16317
+ //#region src/yui_table_select.js
16318
+ /***********************************************************************
16319
+ * yui_table_select.js
16320
+ *
16321
+ * SELECT ROWS IN A TABLE, and act on the lot.
16322
+ *
16323
+ * Deleting twenty rows one confirmation at a time is not a
16324
+ * workflow, it is a punishment, and every table that lets you
16325
+ * remove a row will eventually be asked to remove twenty. This
16326
+ * is that facility, once: the checkbox column, the settings
16327
+ * that make selection behave, and the bar that appears while
16328
+ * something is selected and carries what can be done to it.
16329
+ *
16330
+ * Two decisions are baked in, both learned in the treedb
16331
+ * topic table:
16332
+ *
16333
+ * - Selection is driven ONLY by the checkbox, never by
16334
+ * clicking the row (`selectableRows: "highlight"`). A table
16335
+ * row is full of things to click -- an editor, an icon, a
16336
+ * nested table -- and click-to-select ticks a row every time
16337
+ * you reach for one of them.
16338
+ *
16339
+ * - The header checkbox covers the ACTIVE rows, the ones the
16340
+ * filters leave on screen. "Select all" over rows nobody can
16341
+ * see is how a filtered delete takes the whole topic with
16342
+ * it.
16343
+ *
16344
+ * The bar takes its words from the HOST's `t`: this library
16345
+ * translates through the app's i18next, and the app owns the
16346
+ * keys. It needs "{{n}} selected" and "clear selection"; each
16347
+ * action brings its own key.
16348
+ *
16349
+ * Copyright (c) 2026, ArtGins.
16350
+ * All Rights Reserved.
16351
+ ***********************************************************************/
16352
+ /***************************************************************
16353
+ * The checkbox column. Put it FIRST in the column list.
16354
+ *
16355
+ * opts:
16356
+ * field the (virtual) field name, default "_select"
16357
+ * width default 44
16358
+ * visible default true; `false` for a table that reveals
16359
+ * the column only in an edit mode (the treedb topic
16360
+ * table shows it with `showColumn(field)`)
16361
+ *
16362
+ * This is the SELECT-MANY widget. A table that lets you pick ONE
16363
+ * row uses a radio column of its own (`formatter: "rowSelection"`
16364
+ * with no `titleFormatter`, `selectableRows: 1`): the header
16365
+ * checkbox, the count and the bar all mean nothing there.
16366
+ ***************************************************************/
16367
+ function yui_selection_column(opts) {
16368
+ let o = opts || {};
16369
+ return {
16370
+ title: "",
16371
+ field: o.field || "_select",
16372
+ width: o.width || 44,
16373
+ minWidth: o.width || 44,
16374
+ visible: o.visible !== false,
16375
+ hozAlign: "center",
16376
+ headerHozAlign: "center",
16377
+ headerSort: false,
16378
+ resizable: false,
16379
+ formatter: "rowSelection",
16380
+ titleFormatter: "rowSelection",
16381
+ titleFormatterParams: { rowRange: "active" },
16382
+ cssClass: "TABLE_SELECT_CELL"
16383
+ };
16384
+ }
16385
+ /***************************************************************
16386
+ * What the Tabulator settings need for the column above.
16387
+ * Spread it into the settings object.
16388
+ ***************************************************************/
16389
+ function yui_selection_settings() {
16390
+ return { selectableRows: "highlight" };
16391
+ }
16392
+ /***************************************************************
16393
+ * The rows selected right now (never null).
16394
+ ***************************************************************/
16395
+ function yui_selected_rows(tabulator) {
16396
+ if (!tabulator || typeof tabulator.getSelectedData !== "function") return [];
16397
+ try {
16398
+ return tabulator.getSelectedData() || [];
16399
+ } catch (e) {
16400
+ return [];
16401
+ }
16402
+ }
16403
+ /***************************************************************
16404
+ * Drop the selection (after acting on it, or after a
16405
+ * reload that leaves the ticked rows behind).
16406
+ ***************************************************************/
16407
+ function yui_clear_selection(tabulator) {
16408
+ if (!tabulator || typeof tabulator.deselectRow !== "function") return;
16409
+ try {
16410
+ tabulator.deselectRow();
16411
+ } catch (e) {}
16412
+ }
16413
+ /***************************************************************
16414
+ * The bar that appears while rows are selected.
16415
+ *
16416
+ * t the HOST's translator
16417
+ * opts:
16418
+ * actions [{label, icon, class, on_click}] -- `label` is an
16419
+ * i18n KEY, `class` extra Bulma classes for the
16420
+ * button (e.g. "is-danger"), `on_click` the DOM
16421
+ * handler, whose job is to send an event
16422
+ * on_clear called by the "clear selection" button
16423
+ * name logical-class prefix, default "TABLE"
16424
+ *
16425
+ * -> {$el, set_count(n), refresh()}
16426
+ *
16427
+ * `refresh()` redraws it in the current language: the count is
16428
+ * composed at render time, so it cannot re-translate itself
16429
+ * through `refresh_language()`. Call it on EV_LANGUAGE_CHANGED.
16430
+ ***************************************************************/
16431
+ function yui_selection_bar(t, opts) {
16432
+ let o = opts || {};
16433
+ let actions = Array.isArray(o.actions) ? o.actions : [];
16434
+ let name = o.name || "TABLE";
16435
+ let count = 0;
16436
+ let $count = createElement2([
16437
+ "span",
16438
+ { class: `${name}_SELECTION_COUNT has-text-weight-semibold` },
16439
+ ""
16440
+ ]);
16441
+ let $buttons = [];
16442
+ for (let action of actions) {
16443
+ let $b = createElement2([
16444
+ "button",
16445
+ {
16446
+ class: `${name}_SELECTION_ACTION button ${action.class || ""}`,
16447
+ type: "button"
16448
+ },
16449
+ [[
16450
+ "span",
16451
+ { class: "icon" },
16452
+ [["i", { class: action.icon || "yi-check" }]]
16453
+ ], [
16454
+ "span",
16455
+ {},
16456
+ ""
16457
+ ]],
16458
+ { click: action.on_click }
16459
+ ]);
16460
+ $b._yui_label = action.label || "";
16461
+ $buttons.push($b);
16462
+ }
16463
+ let $clear = createElement2([
16464
+ "button",
16465
+ {
16466
+ class: `${name}_SELECTION_CLEAR button is-ghost`,
16467
+ type: "button"
16468
+ },
16469
+ [[
16470
+ "span",
16471
+ { class: "icon" },
16472
+ [["i", { class: "yi-xmark" }]]
16473
+ ], [
16474
+ "span",
16475
+ {},
16476
+ ""
16477
+ ]],
16478
+ { click: function(e) {
16479
+ if (typeof o.on_clear === "function") o.on_clear(e);
16480
+ } }
16481
+ ]);
16482
+ $clear._yui_label = "clear selection";
16483
+ let $el = createElement2([
16484
+ "div",
16485
+ {
16486
+ class: `${name}_SELECTION_BAR is-align-items-center is-flex-wrap-wrap mb-2 is-hidden`,
16487
+ style: "display:flex; gap:0.5rem;"
16488
+ },
16489
+ [$count].concat($buttons, [$clear])
16490
+ ]);
16491
+ function label_of($b) {
16492
+ return $b.querySelector("span.icon ~ span");
16493
+ }
16494
+ function refresh() {
16495
+ $count.textContent = t("{{n}} selected", { n: count });
16496
+ for (let $b of $buttons.concat([$clear])) {
16497
+ let $label = label_of($b);
16498
+ let key = $b._yui_label;
16499
+ if ($label && key) $label.textContent = t(key);
16500
+ $b.title = key ? t(key) : "";
16501
+ $b.setAttribute("aria-label", $b.title);
16502
+ }
16503
+ $el.classList.toggle("is-hidden", count <= 0);
16504
+ }
16505
+ function set_count(n) {
16506
+ count = n > 0 ? n : 0;
16507
+ refresh();
16508
+ }
16509
+ refresh();
16510
+ return {
16511
+ $el,
16512
+ set_count,
16513
+ refresh
16514
+ };
16515
+ }
16516
+ //#endregion
16315
16517
  //#region src/treedb_write_plan.js
16316
16518
  /***********************************************************************
16317
16519
  * treedb_write_plan.js
@@ -16479,6 +16681,7 @@ var attrs_table$5 = [
16479
16681
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16480
16682
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
16481
16683
  SDATA(data_type_t.DTP_BOOLEAN, "with_radio", 0, false, "Auxiliary first column to select one row"),
16684
+ SDATA(data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Show the shared selection bar (\"N selected\" + a way out) above the table while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, and a host that has not defined \"{{n}} selected\" and \"clear selection\" would render the keys"),
16482
16685
  SDATA(data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "Broadcast select rows event"),
16483
16686
  SDATA(data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "Broadcast unselect rows event"),
16484
16687
  SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
@@ -16497,6 +16700,7 @@ var attrs_table$5 = [
16497
16700
  }, "Default settings for Tabulator"),
16498
16701
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTML container for UI"),
16499
16702
  SDATA(data_type_t.DTP_POINTER, "tabulator", 0, null, "Tabulator instance"),
16703
+ SDATA(data_type_t.DTP_POINTER, "selection_bar", 0, null, "The selection bar (yui_table_select.js), when with_selection_bar"),
16500
16704
  SDATA(data_type_t.DTP_STRING, "table_id", 0, null, "Table div ID"),
16501
16705
  SDATA(data_type_t.DTP_STRING, "toolbar_id", 0, null, "Toolbar ID"),
16502
16706
  SDATA(data_type_t.DTP_STRING, "popup_id", 0, null, "Edit form popup ID"),
@@ -16913,6 +17117,15 @@ function build_ui$5(gobj) {
16913
17117
  style: "margin-top:0px !important;"
16914
17118
  }]]
16915
17119
  ]);
17120
+ if (gobj_read_bool_attr(gobj, "with_checkbox") && gobj_read_bool_attr(gobj, "with_selection_bar")) {
17121
+ let bar = yui_selection_bar(t, {
17122
+ name: "TREEDB_TABLE",
17123
+ actions: [],
17124
+ on_clear: () => gobj_send_event(gobj, "EV_CLEAR_SELECTION", {}, gobj)
17125
+ });
17126
+ gobj_write_attr(gobj, "selection_bar", bar);
17127
+ $container.insertBefore(bar.$el, $container.querySelector(`#${table_id}`));
17128
+ }
16916
17129
  let $toolbar_slot = $container.querySelector(".toolbar_tabulator_table");
16917
17130
  if ($table_toolbar instanceof Element) $toolbar_slot.appendChild($table_toolbar);
16918
17131
  else $toolbar_slot.appendChild(createElement2(["div", {}]));
@@ -16921,6 +17134,24 @@ function build_ui$5(gobj) {
16921
17134
  refresh_language($container, t);
16922
17135
  }
16923
17136
  /************************************************************
17137
+ * How many rows are ticked, on the bar.
17138
+ *
17139
+ * Only while the table is EDITABLE: outside edition its checkbox
17140
+ * column is hidden (`hideColumn`), and a count of rows nobody can
17141
+ * see or untick is a count you cannot act on. The selection itself
17142
+ * is left alone -- coming back to edition finds it, and the bar
17143
+ * recounts from the table.
17144
+ ************************************************************/
17145
+ function render_selection_bar(gobj) {
17146
+ let bar = gobj_read_attr(gobj, "selection_bar");
17147
+ if (!bar) return;
17148
+ if (!gobj_read_bool_attr(gobj, "editable")) {
17149
+ bar.set_count(0);
17150
+ return;
17151
+ }
17152
+ bar.set_count(yui_selected_rows(gobj_read_attr(gobj, "tabulator")).length);
17153
+ }
17154
+ /************************************************************
16924
17155
  * Destroy UI
16925
17156
  ************************************************************/
16926
17157
  function destroy_ui$2(gobj) {
@@ -16991,16 +17222,11 @@ function create_tabulator(gobj) {
16991
17222
  let columns = [];
16992
17223
  let with_checkbox = gobj_read_bool_attr(gobj, "with_checkbox");
16993
17224
  let with_radio = gobj_read_bool_attr(gobj, "with_radio");
16994
- if (with_checkbox) columns.push({
16995
- formatter: "rowSelection",
16996
- titleFormatter: "rowSelection",
17225
+ if (with_checkbox) columns.push(yui_selection_column({
16997
17226
  field: "_check_box_state_",
16998
- hozAlign: "center",
16999
- headerHozAlign: "center",
17000
17227
  width: 40,
17001
- visible: false,
17002
- headerSort: false
17003
- });
17228
+ visible: false
17229
+ }));
17004
17230
  else if (with_radio) columns.push({
17005
17231
  formatter: "rowSelection",
17006
17232
  field: "_check_box_state_",
@@ -17229,7 +17455,9 @@ function create_tabulator(gobj) {
17229
17455
  }
17230
17456
  });
17231
17457
  let pkey = desc.pkey || "id";
17232
- let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
17458
+ let selectable = false;
17459
+ if (with_checkbox) selectable = yui_selection_settings().selectableRows;
17460
+ else if (with_radio) selectable = 1;
17233
17461
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
17234
17462
  if (gobj_read_bool_attr(gobj, "with_remote_paging")) {
17235
17463
  let page_size = gobj_read_integer_attr(gobj, "page_size") || 200;
@@ -18113,6 +18341,8 @@ function refuse_if_readonly$2(gobj, event) {
18113
18341
  * dict. Re-apply it in the new language.
18114
18342
  ***************************************************************/
18115
18343
  function ac_language_changed$2(gobj, event, kw, src) {
18344
+ let bar = gobj_read_attr(gobj, "selection_bar");
18345
+ if (bar) bar.refresh();
18116
18346
  let tabulator = gobj_read_attr(gobj, "tabulator");
18117
18347
  if (!tabulator) return 0;
18118
18348
  yui_tabulator_relocalize(tabulator, t);
@@ -18211,7 +18441,7 @@ function ac_edition_mode(gobj, event, kw, src) {
18211
18441
  tabulator.showColumn("_check_box_state_");
18212
18442
  $button_new_record.removeAttribute("disabled");
18213
18443
  $button_paste_record.removeAttribute("disabled");
18214
- if (tabulator.getSelectedData().length) {
18444
+ if (yui_selected_rows(tabulator).length) {
18215
18445
  $button_delete_record.removeAttribute("disabled");
18216
18446
  $button_copy_record.removeAttribute("disabled");
18217
18447
  }
@@ -18226,6 +18456,7 @@ function ac_edition_mode(gobj, event, kw, src) {
18226
18456
  $button_copy_record.setAttribute("disabled", true);
18227
18457
  $button_paste_record.setAttribute("disabled", true);
18228
18458
  }
18459
+ render_selection_bar(gobj);
18229
18460
  return 0;
18230
18461
  }
18231
18462
  /************************************************************
@@ -18269,7 +18500,7 @@ function ac_delete_rows(gobj, event, kw, src) {
18269
18500
  if (refuse_if_readonly$2(gobj, event)) return -1;
18270
18501
  let tabulator = gobj_read_attr(gobj, "tabulator");
18271
18502
  if (json_size(kw) === 0) {
18272
- let rows = tabulator.getSelectedData();
18503
+ let rows = yui_selected_rows(tabulator);
18273
18504
  if (!rows.length) {
18274
18505
  yui_shell_confirm_ok(yui_shell_of(gobj), "please select some row", {
18275
18506
  t,
@@ -18305,7 +18536,7 @@ function ac_delete_rows(gobj, event, kw, src) {
18305
18536
  * From internal
18306
18537
  ************************************************************/
18307
18538
  function ac_copy_rows(gobj, event, kw, src) {
18308
- let rows = gobj_read_attr(gobj, "tabulator").getSelectedData();
18539
+ let rows = yui_selected_rows(gobj_read_attr(gobj, "tabulator"));
18309
18540
  if (!rows.length) {
18310
18541
  yui_shell_confirm_ok(yui_shell_of(gobj), "please select some row", {
18311
18542
  t,
@@ -18363,7 +18594,7 @@ function ac_select_rows(gobj, event, kw, src) {
18363
18594
  let $button_delete_record = $container.querySelector(`.button-delete-record`);
18364
18595
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
18365
18596
  if ($button_delete_record) {
18366
- if (tabulator.getSelectedData().length && gobj_read_bool_attr(gobj, "editable")) {
18597
+ if (yui_selected_rows(tabulator).length && gobj_read_bool_attr(gobj, "editable")) {
18367
18598
  $button_delete_record.removeAttribute("disabled");
18368
18599
  $button_copy_record.removeAttribute("disabled");
18369
18600
  } else {
@@ -18371,6 +18602,7 @@ function ac_select_rows(gobj, event, kw, src) {
18371
18602
  $button_copy_record.setAttribute("disabled", true);
18372
18603
  }
18373
18604
  }
18605
+ render_selection_bar(gobj);
18374
18606
  if (gobj_read_bool_attr(gobj, "broadcast_select_rows_event")) gobj_publish_event(gobj, event, kw);
18375
18607
  return 0;
18376
18608
  }
@@ -18383,7 +18615,7 @@ function ac_unselect_rows(gobj, event, kw, src) {
18383
18615
  let $button_delete_record = $container.querySelector(`.button-delete-record`);
18384
18616
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
18385
18617
  if ($button_delete_record) {
18386
- if (tabulator.getSelectedData().length && gobj_read_bool_attr(gobj, "editable")) {
18618
+ if (yui_selected_rows(tabulator).length && gobj_read_bool_attr(gobj, "editable")) {
18387
18619
  $button_delete_record.removeAttribute("disabled");
18388
18620
  $button_copy_record.removeAttribute("disabled");
18389
18621
  } else {
@@ -18391,10 +18623,19 @@ function ac_unselect_rows(gobj, event, kw, src) {
18391
18623
  $button_copy_record.setAttribute("disabled", true);
18392
18624
  }
18393
18625
  }
18626
+ render_selection_bar(gobj);
18394
18627
  if (gobj_read_bool_attr(gobj, "broadcast_select_rows_event")) gobj_publish_event(gobj, event, kw);
18395
18628
  return 0;
18396
18629
  }
18397
18630
  /************************************************************
18631
+ * The way out of a selection (the bar's clear button).
18632
+ ************************************************************/
18633
+ function ac_clear_selection(gobj, event, kw, src) {
18634
+ yui_clear_selection(gobj_read_attr(gobj, "tabulator"));
18635
+ render_selection_bar(gobj);
18636
+ return 0;
18637
+ }
18638
+ /************************************************************
18398
18639
  * { locale: "es" }
18399
18640
  ************************************************************/
18400
18641
  function ac_change_locale(gobj, event, kw, src) {
@@ -18871,6 +19112,11 @@ function create_gclass$5(gclass_name) {
18871
19112
  ac_select_rows,
18872
19113
  null
18873
19114
  ],
19115
+ [
19116
+ "EV_CLEAR_SELECTION",
19117
+ ac_clear_selection,
19118
+ null
19119
+ ],
18874
19120
  [
18875
19121
  "EV_UNSELECT_ROWS",
18876
19122
  ac_unselect_rows,
@@ -18979,6 +19225,7 @@ function create_gclass$5(gclass_name) {
18979
19225
  ["EV_DELETE_ROWS", 0],
18980
19226
  ["EV_COPY_ROWS", 0],
18981
19227
  ["EV_PASTE_ROWS", 0],
19228
+ ["EV_CLEAR_SELECTION", 0],
18982
19229
  ["EV_SELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
18983
19230
  ["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
18984
19231
  ["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.11.1",
3
+ "version": "7.13.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -75,6 +75,14 @@ import {attach_clear} from "./yui_inputs.js";
75
75
 
76
76
  import {yui_tabulator_lang, yui_tabulator_relocalize} from "./yui_tabulator_i18n.js";
77
77
 
78
+ import {
79
+ yui_selection_column,
80
+ yui_selection_settings,
81
+ yui_selection_bar,
82
+ yui_selected_rows,
83
+ yui_clear_selection,
84
+ } from "./yui_table_select.js";
85
+
78
86
  import {t} from "i18next";
79
87
 
80
88
  import {plan_treedb_writes} from "./treedb_write_plan.js";
@@ -123,6 +131,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"
123
131
  /*---------------- Selection Mode ----------------*/
124
132
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
125
133
  SDATA(data_type_t.DTP_BOOLEAN, "with_radio", 0, false, "Auxiliary first column to select one row"),
134
+ SDATA(data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Show the shared selection bar (\"N selected\" + a way out) above the table while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, and a host that has not defined \"{{n}} selected\" and \"clear selection\" would render the keys"),
126
135
  SDATA(data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "Broadcast select rows event"),
127
136
  SDATA(data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "Broadcast unselect rows event"),
128
137
 
@@ -142,6 +151,7 @@ SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
142
151
  /*---------------- Internal Attributes ----------------*/
143
152
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTML container for UI"),
144
153
  SDATA(data_type_t.DTP_POINTER, "tabulator", 0, null, "Tabulator instance"),
154
+ SDATA(data_type_t.DTP_POINTER, "selection_bar", 0, null, "The selection bar (yui_table_select.js), when with_selection_bar"),
145
155
  SDATA(data_type_t.DTP_STRING, "table_id", 0, null, "Table div ID"),
146
156
  SDATA(data_type_t.DTP_STRING, "toolbar_id", 0, null, "Toolbar ID"),
147
157
  SDATA(data_type_t.DTP_STRING, "popup_id", 0, null, "Edit form popup ID"),
@@ -642,6 +652,21 @@ function build_ui(gobj)
642
652
  ]
643
653
  ]]
644
654
  );
655
+ /* The shared selection bar, between the toolbar and the table. It is
656
+ * the only thing that says HOW MANY rows are ticked: with 200 rows to a
657
+ * page, the checkboxes that answer that question are not all on screen.
658
+ * Opt-in, because it takes its words from the host's i18n. */
659
+ if(gobj_read_bool_attr(gobj, "with_checkbox") &&
660
+ gobj_read_bool_attr(gobj, "with_selection_bar")) {
661
+ let bar = yui_selection_bar(t, {
662
+ name: "TREEDB_TABLE",
663
+ actions: [], /* the toolbar above already carries them */
664
+ on_clear: () => gobj_send_event(gobj, "EV_CLEAR_SELECTION", {}, gobj)
665
+ });
666
+ gobj_write_attr(gobj, "selection_bar", bar);
667
+ $container.insertBefore(bar.$el, $container.querySelector(`#${table_id}`));
668
+ }
669
+
645
670
  let $toolbar_slot = $container.querySelector('.toolbar_tabulator_table');
646
671
  if($table_toolbar instanceof Element) {
647
672
  $toolbar_slot.appendChild($table_toolbar);
@@ -654,6 +679,28 @@ function build_ui(gobj)
654
679
  refresh_language($container, t);
655
680
  }
656
681
 
682
+ /************************************************************
683
+ * How many rows are ticked, on the bar.
684
+ *
685
+ * Only while the table is EDITABLE: outside edition its checkbox
686
+ * column is hidden (`hideColumn`), and a count of rows nobody can
687
+ * see or untick is a count you cannot act on. The selection itself
688
+ * is left alone -- coming back to edition finds it, and the bar
689
+ * recounts from the table.
690
+ ************************************************************/
691
+ function render_selection_bar(gobj)
692
+ {
693
+ let bar = gobj_read_attr(gobj, "selection_bar");
694
+ if(!bar) {
695
+ return;
696
+ }
697
+ if(!gobj_read_bool_attr(gobj, "editable")) {
698
+ bar.set_count(0);
699
+ return;
700
+ }
701
+ bar.set_count(yui_selected_rows(gobj_read_attr(gobj, "tabulator")).length);
702
+ }
703
+
657
704
  /************************************************************
658
705
  * Destroy UI
659
706
  ************************************************************/
@@ -748,16 +795,20 @@ function create_tabulator(gobj)
748
795
  let with_checkbox = gobj_read_bool_attr(gobj, "with_checkbox");
749
796
  let with_radio = gobj_read_bool_attr(gobj, "with_radio");
750
797
  if(with_checkbox) {
751
- columns.push({
752
- formatter: "rowSelection",
753
- titleFormatter: "rowSelection",
754
- field: "_check_box_state_", // WARNING _check_box_state_ widely used
755
- hozAlign: "center",
756
- headerHozAlign: "center",
757
- width: 40,
758
- visible: false,
759
- headerSort: false
760
- });
798
+ /* The SHARED column (yui_table_select.js), not a copy of it: this
799
+ * table is where its two decisions were learned, and it was the one
800
+ * table not applying the second. Its header checkbox took
801
+ * `selectRow(undefined)` -- Tabulator's "every row LOADED", the ones
802
+ * the header filters hide included -- and the button beside it
803
+ * DELETES. It now ticks the active rows, the ones on screen.
804
+ *
805
+ * Still `visible: false` and still 40px wide: this table reveals the
806
+ * column in edit mode (`showColumn`), and the geometry is its own. */
807
+ columns.push(yui_selection_column({
808
+ field: "_check_box_state_", // WARNING _check_box_state_ widely used
809
+ width: 40,
810
+ visible: false
811
+ }));
761
812
  } else if(with_radio) {
762
813
  columns.push({
763
814
  formatter: "rowSelection",
@@ -1098,7 +1149,12 @@ function create_tabulator(gobj)
1098
1149
  * (it calls toggleSelect() directly) while disabling click-to-select,
1099
1150
  * so opening the edit (yi-pen) form no longer implicitly ticks the
1100
1151
  * row. Radio keeps single click-select (its own widget). */
1101
- let selectable = with_checkbox ? "highlight" : (with_radio ? 1 : false);
1152
+ let selectable = false;
1153
+ if(with_checkbox) {
1154
+ selectable = yui_selection_settings().selectableRows;
1155
+ } else if(with_radio) {
1156
+ selectable = 1; /* pick ONE: the radio's own widget */
1157
+ }
1102
1158
 
1103
1159
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
1104
1160
 
@@ -2420,6 +2476,10 @@ function refuse_if_readonly(gobj, event)
2420
2476
  ***************************************************************/
2421
2477
  function ac_language_changed(gobj, event, kw, src)
2422
2478
  {
2479
+ let bar = gobj_read_attr(gobj, "selection_bar");
2480
+ if(bar) {
2481
+ bar.refresh(); /* a count composed at render time */
2482
+ }
2423
2483
  let tabulator = gobj_read_attr(gobj, "tabulator");
2424
2484
  if(!tabulator) {
2425
2485
  return 0;
@@ -2623,7 +2683,7 @@ function ac_edition_mode(gobj, event, kw, src)
2623
2683
  $button_new_record.removeAttribute("disabled");
2624
2684
  $button_paste_record.removeAttribute("disabled");
2625
2685
 
2626
- let rows = tabulator.getSelectedData();
2686
+ let rows = yui_selected_rows(tabulator);
2627
2687
  if (rows.length) {
2628
2688
  $button_delete_record.removeAttribute("disabled");
2629
2689
  $button_copy_record.removeAttribute("disabled");
@@ -2646,6 +2706,8 @@ function ac_edition_mode(gobj, event, kw, src)
2646
2706
  $button_paste_record.setAttribute("disabled", true);
2647
2707
  }
2648
2708
 
2709
+ render_selection_bar(gobj);
2710
+
2649
2711
  return 0;
2650
2712
  }
2651
2713
 
@@ -2715,7 +2777,7 @@ function ac_delete_rows(gobj, event, kw, src)
2715
2777
  /*----------------------------*
2716
2778
  * Delete selected rows
2717
2779
  *----------------------------*/
2718
- let rows = tabulator.getSelectedData();
2780
+ let rows = yui_selected_rows(tabulator);
2719
2781
  if (!rows.length) {
2720
2782
  yui_shell_confirm_ok(
2721
2783
  yui_shell_of(gobj), 'please select some row',
@@ -2778,7 +2840,7 @@ function ac_copy_rows(gobj, event, kw, src)
2778
2840
  /*----------------------------*
2779
2841
  * Copy selected rows
2780
2842
  *----------------------------*/
2781
- let rows = tabulator.getSelectedData();
2843
+ let rows = yui_selected_rows(tabulator);
2782
2844
  if (!rows.length) {
2783
2845
  yui_shell_confirm_ok(
2784
2846
  yui_shell_of(gobj), 'please select some row',
@@ -2875,7 +2937,7 @@ function ac_select_rows(gobj, event, kw, src)
2875
2937
  let $button_delete_record = $container.querySelector(`.button-delete-record`);
2876
2938
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
2877
2939
  if($button_delete_record) {
2878
- let selectedRows = tabulator.getSelectedData();
2940
+ let selectedRows = yui_selected_rows(tabulator);
2879
2941
  if (selectedRows.length && gobj_read_bool_attr(gobj, "editable")) {
2880
2942
  $button_delete_record.removeAttribute("disabled");
2881
2943
  $button_copy_record.removeAttribute("disabled");
@@ -2885,6 +2947,8 @@ function ac_select_rows(gobj, event, kw, src)
2885
2947
  }
2886
2948
  }
2887
2949
 
2950
+ render_selection_bar(gobj);
2951
+
2888
2952
  if(gobj_read_bool_attr(gobj, "broadcast_select_rows_event")) {
2889
2953
  gobj_publish_event(gobj, event, kw);
2890
2954
  }
@@ -2906,7 +2970,7 @@ function ac_unselect_rows(gobj, event, kw, src)
2906
2970
  let $button_delete_record = $container.querySelector(`.button-delete-record`);
2907
2971
  let $button_copy_record = $container.querySelector(`.button-copy-record`);
2908
2972
  if($button_delete_record) {
2909
- let selectedRows = tabulator.getSelectedData();
2973
+ let selectedRows = yui_selected_rows(tabulator);
2910
2974
  if (selectedRows.length && gobj_read_bool_attr(gobj, "editable")) {
2911
2975
  $button_delete_record.removeAttribute("disabled");
2912
2976
  $button_copy_record.removeAttribute("disabled");
@@ -2916,7 +2980,10 @@ function ac_unselect_rows(gobj, event, kw, src)
2916
2980
  }
2917
2981
  }
2918
2982
 
2983
+ render_selection_bar(gobj);
2984
+
2919
2985
  // WARNING with radio, there is no unselect event.
2986
+
2920
2987
  if(gobj_read_bool_attr(gobj, "broadcast_select_rows_event")) {
2921
2988
  gobj_publish_event(gobj, event, kw);
2922
2989
  }
@@ -2924,6 +2991,16 @@ function ac_unselect_rows(gobj, event, kw, src)
2924
2991
  return 0;
2925
2992
  }
2926
2993
 
2994
+ /************************************************************
2995
+ * The way out of a selection (the bar's clear button).
2996
+ ************************************************************/
2997
+ function ac_clear_selection(gobj, event, kw, src)
2998
+ {
2999
+ yui_clear_selection(gobj_read_attr(gobj, "tabulator"));
3000
+ render_selection_bar(gobj);
3001
+ return 0;
3002
+ }
3003
+
2927
3004
  /************************************************************
2928
3005
  * { locale: "es" }
2929
3006
  ************************************************************/
@@ -3432,6 +3509,7 @@ function create_gclass(gclass_name)
3432
3509
  ["EV_NEW_ROW", ac_new_row, null],
3433
3510
  ["EV_DELETE_ROWS", ac_delete_rows, null],
3434
3511
  ["EV_SELECT_ROWS", ac_select_rows, null],
3512
+ ["EV_CLEAR_SELECTION", ac_clear_selection, null],
3435
3513
  ["EV_UNSELECT_ROWS", ac_unselect_rows, null],
3436
3514
  ["EV_COPY_ROWS", ac_copy_rows, null],
3437
3515
  ["EV_PASTE_ROWS", ac_paste_rows, null],
@@ -3470,6 +3548,7 @@ function create_gclass(gclass_name)
3470
3548
  ["EV_DELETE_ROWS", 0],
3471
3549
  ["EV_COPY_ROWS", 0],
3472
3550
  ["EV_PASTE_ROWS", 0],
3551
+ ["EV_CLEAR_SELECTION", 0],
3473
3552
  ["EV_SELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
3474
3553
  ["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
3475
3554
  ["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
@@ -76,6 +76,7 @@ SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for
76
76
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
77
77
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
78
78
  SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. Safe against a backend that cannot page: it answers the whole list, which the table reads as one page"),
79
+ SDATA(data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Each topic table shows the shared selection bar (\"N selected\" + a way out) above it while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, so a host that turns it on must define \"{{n}} selected\" and \"clear selection\""),
79
80
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
80
81
  SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
81
82
  SDATA(data_type_t.DTP_BOOLEAN, "with_cards_landing",0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
@@ -1039,7 +1040,8 @@ function process_treedb_descs(gobj)
1039
1040
  treedb_name: treedb_name,
1040
1041
  topic_name: key,
1041
1042
  desc: desc,
1042
- with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging")
1043
+ with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging"),
1044
+ with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar")
1043
1045
  };
1044
1046
 
1045
1047
  let id = `${gobj_name(gobj)}?${desc.topic_name}`;
package/src/tabulator.css CHANGED
@@ -220,9 +220,7 @@
220
220
  */
221
221
  [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing input,
222
222
  [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing select,
223
- [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing textarea,
224
- [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input,
225
- [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter select {
223
+ [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing textarea {
226
224
  background-color: var(--bulma-grey-darker, #2b2f38);
227
225
  color: var(--bulma-text-strong, #f5f6f7) !important;
228
226
  caret-color: var(--bulma-text-strong, #f5f6f7);
@@ -234,6 +232,44 @@
234
232
  color: var(--bulma-text-weak, #8a8f98);
235
233
  }
236
234
 
235
+ /* ====================================================================
236
+ * The HEADER FILTER box is FURNITURE, not an open editor.
237
+ *
238
+ * Both are inputs inside the table, so they shared these rules -- but an
239
+ * editor is a MODE: one cell, while you type into it, and deliberately the
240
+ * brightest thing on screen. A filter box is permanent and there is one per
241
+ * column, so wearing the editor's border a header of eight columns drew
242
+ * eight hard boxes across the top of every table, link-BLUE in dark theme.
243
+ *
244
+ * Discreet at rest -- a hairline in the theme's weakest border colour, no
245
+ * wash of its own -- and only the FOCUSED one stands out, which is the one
246
+ * you are typing in.
247
+ * ==================================================================== */
248
+ .tabulator .tabulator-header .tabulator-header-filter input,
249
+ .tabulator .tabulator-header .tabulator-header-filter select {
250
+ border: 1px solid var(--bulma-border-weak, #e8eaee);
251
+ border-radius: 3px;
252
+ background-color: transparent;
253
+ }
254
+
255
+ .tabulator .tabulator-header .tabulator-header-filter input:focus,
256
+ .tabulator .tabulator-header .tabulator-header-filter select:focus {
257
+ border-color: var(--bulma-link, #4a9eff);
258
+ outline: none;
259
+ }
260
+
261
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input,
262
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter select {
263
+ border-color: var(--bulma-border, #3a3f4a);
264
+ color: var(--bulma-text-strong, #f5f6f7);
265
+ caret-color: var(--bulma-text-strong, #f5f6f7);
266
+ }
267
+
268
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input:focus,
269
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter select:focus {
270
+ border-color: var(--bulma-link, #4a9eff);
271
+ }
272
+
237
273
  [data-theme=dark] .tabulator-row.tabulator-selected {
238
274
  background-color: var(--bulma-link) !important;
239
275
  color: var(--bulma-link-invert) !important;
@@ -313,9 +349,7 @@
313
349
  /* same editor fix as [data-theme=dark] above */
314
350
  [data-theme=system] .tabulator-row .tabulator-cell.tabulator-editing input,
315
351
  [data-theme=system] .tabulator-row .tabulator-cell.tabulator-editing select,
316
- [data-theme=system] .tabulator-row .tabulator-cell.tabulator-editing textarea,
317
- [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter input,
318
- [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter select {
352
+ [data-theme=system] .tabulator-row .tabulator-cell.tabulator-editing textarea {
319
353
  background-color: var(--bulma-grey-darker, #2b2f38);
320
354
  color: var(--bulma-text-strong, #f5f6f7) !important;
321
355
  caret-color: var(--bulma-text-strong, #f5f6f7);
@@ -325,6 +359,17 @@
325
359
  [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter input::placeholder {
326
360
  color: var(--bulma-text-weak, #8a8f98);
327
361
  }
362
+ /* same furniture-not-editor rule as [data-theme=dark] above */
363
+ [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter input,
364
+ [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter select {
365
+ border-color: var(--bulma-border, #3a3f4a);
366
+ color: var(--bulma-text-strong, #f5f6f7);
367
+ caret-color: var(--bulma-text-strong, #f5f6f7);
368
+ }
369
+ [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter input:focus,
370
+ [data-theme=system] .tabulator .tabulator-header .tabulator-header-filter select:focus {
371
+ border-color: var(--bulma-link, #4a9eff);
372
+ }
328
373
  [data-theme=system] .tabulator-row.tabulator-selected {
329
374
  background-color: var(--bulma-link) !important;
330
375
  color: var(--bulma-link-invert) !important;
@@ -38,8 +38,16 @@ import {createElement2, log_error} from "@yuneta/gobj-js";
38
38
  * The checkbox column. Put it FIRST in the column list.
39
39
  *
40
40
  * opts:
41
- * field the (virtual) field name, default "_select"
42
- * width default 44
41
+ * field the (virtual) field name, default "_select"
42
+ * width default 44
43
+ * visible default true; `false` for a table that reveals
44
+ * the column only in an edit mode (the treedb topic
45
+ * table shows it with `showColumn(field)`)
46
+ *
47
+ * This is the SELECT-MANY widget. A table that lets you pick ONE
48
+ * row uses a radio column of its own (`formatter: "rowSelection"`
49
+ * with no `titleFormatter`, `selectableRows: 1`): the header
50
+ * checkbox, the count and the bar all mean nothing there.
43
51
  ***************************************************************/
44
52
  function yui_selection_column(opts)
45
53
  {
@@ -49,6 +57,7 @@ function yui_selection_column(opts)
49
57
  field: o.field || "_select",
50
58
  width: o.width || 44,
51
59
  minWidth: o.width || 44,
60
+ visible: o.visible !== false,
52
61
  hozAlign: "center",
53
62
  headerHozAlign: "center",
54
63
  headerSort: false,