@yuneta/gobj-ui 7.23.51 → 7.23.53

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.
@@ -86,6 +86,7 @@ import {
86
86
  } from "./yui_table_select.js";
87
87
 
88
88
  import {yui_table_filter_clear} from "./yui_table_filter_clear.js";
89
+ import {yui_icon_is_defined} from "./lib_icons.js";
89
90
  import {row_matches} from "./yui_row_search.js";
90
91
 
91
92
  import {t} from "i18next";
@@ -188,8 +189,8 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button
188
189
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
189
190
  SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
190
191
  SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
191
- SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Pull the topic a PAGE at a time from the backend (`nodes` from/limit) instead of loading it whole. Needs a backend that pages"),
192
- SDATA(data_type_t.DTP_INTEGER, "page_size", 0, 200, "Rows per page when with_remote_paging. Generous on purpose: a treedb that fits in one page behaves exactly as it did, paginator hidden and every filter seeing every row"),
192
+ SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Pull the topic a PAGE at a time from the backend (`nodes` from/limit) instead of loading it whole. Needs a backend that pages. ⚠️ DO NOT TURN THIS ON: it breaks LINKS -- see the note above the tabulator settings. It is off in every consumer until linking works with a partial topic"),
193
+ SDATA(data_type_t.DTP_INTEGER, "page_size", 0, 0, "Rows per page. 0 leaves the `tabulator_settings` value alone. With with_remote_paging it is also the size of each fetch (200 when unset)"),
193
194
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
194
195
 
195
196
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
@@ -1284,6 +1285,14 @@ function create_tabulator(gobj)
1284
1285
  hozAlign = "center";
1285
1286
  colFormatter = "color";
1286
1287
  break;
1288
+ case "icon":
1289
+ /* The NAME of an icon of the app's set, not a file --
1290
+ * which is why `image` cannot stand in for it: it built
1291
+ * an `<img src="yi-bolt">` and drew a broken image. */
1292
+ hozAlign = "center";
1293
+ vertAlign = "middle";
1294
+ colFormatter = icon_formatter;
1295
+ break;
1287
1296
  case "object":
1288
1297
  case "dict":
1289
1298
  case "template":
@@ -1393,16 +1402,29 @@ function create_tabulator(gobj)
1393
1402
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
1394
1403
 
1395
1404
  /* Remote paging: the TABLE pulls, instead of the host pushing the whole
1396
- * topic down. The page size is generous on purpose — a treedb that fits
1397
- * in one page behaves exactly as it did before, with the paginator
1398
- * hidden and every filter seeing every row. Only a topic that does NOT
1399
- * fit pays for paging, and for that one loading it whole was never an
1400
- * option anyway.
1405
+ * topic down. Only a topic that does NOT fit pays for it, and for that
1406
+ * one loading it whole was never an option anyway.
1401
1407
  *
1402
1408
  * `filterMode: "local"` says the plain truth: the header filters and the
1403
1409
  * search box work on the page that is loaded. Same as the tranger
1404
1410
  * browser's Rows card, and for the same reason — the alternative is
1405
1411
  * pushing every filter to the backend and changing what "search" means.
1412
+ *
1413
+ * ⚠️ AND IT IS OFF IN EVERY CONSUMER, because it BREAKS LINKING.
1414
+ * `build_fkey_options()` asks the PARENT topic's table for the rows it
1415
+ * holds (`get_topic_data` -> `tabulator.getData()`), and with paging
1416
+ * that is one page. So the link picker offers 200 of the 5891 possible
1417
+ * parents -- the right one is usually not among them -- and a record
1418
+ * whose parent is not on the loaded page opens with an fkey the form
1419
+ * cannot match, which saving then drops. Local pagination has none of
1420
+ * this: `getData()` answers the WHOLE dataset whatever page is on
1421
+ * screen, so paginating the DISPLAY is safe and only paginating the
1422
+ * FETCH is not.
1423
+ *
1424
+ * A treedb is a memory database -- the backend holds the topic in RAM
1425
+ * either way -- so paging buys little and costs the links. It stays
1426
+ * here, working and tested, for the day linking learns to ask the
1427
+ * BACKEND for a parent instead of the sibling table.
1406
1428
  */
1407
1429
  if(gobj_read_bool_attr(gobj, "with_remote_paging")) {
1408
1430
  let page_size = gobj_read_integer_attr(gobj, "page_size") || 200;
@@ -1428,6 +1450,20 @@ function create_tabulator(gobj)
1428
1450
  });
1429
1451
  }
1430
1452
 
1453
+ /* Rows per page for the LOCAL paginator too, when the host asks for
1454
+ * one: with the fetch no longer paged, `page_size` is the only thing
1455
+ * left that says how many rows a reader wants in front of them, and a
1456
+ * host that had 200 should not drop to 25 for having stopped paging the
1457
+ * fetch. The sizes offered go up with it -- a selector that cannot name
1458
+ * the size in use is a selector Tabulator has to patch itself. */
1459
+ if(!gobj_read_bool_attr(gobj, "with_remote_paging")) {
1460
+ let page_size = gobj_read_integer_attr(gobj, "page_size");
1461
+ if(page_size > 0 && tabulator_settings.pagination) {
1462
+ tabulator_settings.paginationSize = page_size;
1463
+ tabulator_settings.paginationSizeSelector = [50, 100, 200, 500, true];
1464
+ }
1465
+ }
1466
+
1431
1467
  /* The size the reader picked last time, which outranks both defaults
1432
1468
  * above. Applied here, once the two paths have set theirs, so it is
1433
1469
  * written in ONE place -- and applied to `paginationSize` only: the
@@ -1667,6 +1703,48 @@ function table__destroy(gobj)
1667
1703
  }
1668
1704
  }
1669
1705
 
1706
+ /************************************************************
1707
+ * Cell of a col flagged `icon`: the value is the NAME of an
1708
+ * icon of the app's set ("yi-bolt"), so it is DRAWN.
1709
+ *
1710
+ * Built as a DOM node and not as an HTML string: the value is
1711
+ * raw record data and must never be parsed as markup -- and
1712
+ * here it also becomes a CSS class, so it is filtered to the
1713
+ * shape of a class name before it gets near one.
1714
+ *
1715
+ * A name the set does not carry falls back to the TEXT of it.
1716
+ * The catch-all rule `[class^="yi-"]::before` paints a
1717
+ * currentColor box for any name, so drawing it anyway would
1718
+ * render a solid square -- a glyph nobody recognises rather
1719
+ * than a mistake somebody can fix.
1720
+ ************************************************************/
1721
+ function icon_formatter(cell)
1722
+ {
1723
+ let name = cell.getValue();
1724
+ if(!is_string(name) || empty_string(name)) {
1725
+ return "";
1726
+ }
1727
+ name = name.trim();
1728
+
1729
+ if(!/^[a-z0-9-]+$/.test(name) || !yui_icon_is_defined(name)) {
1730
+ /* The NAME, in plain grey, and no invented label around it: it
1731
+ * is a developer's problem (a schema naming a glyph the set
1732
+ * does not carry) and the name IS the whole diagnosis -- while
1733
+ * a sentence here would be one more i18n key for every app
1734
+ * that mounts this table. */
1735
+ return createElement2([
1736
+ "span",
1737
+ {class: "ICON_CELL_UNKNOWN is-size-7 has-text-grey", title: name},
1738
+ name
1739
+ ]);
1740
+ }
1741
+
1742
+ return createElement2([
1743
+ "span",
1744
+ {class: `ICON_CELL ${name}`, title: name, "aria-label": name}
1745
+ ]);
1746
+ }
1747
+
1670
1748
  /************************************************************
1671
1749
  * Cell of a col holding a JSON document (dict/list/blob/
1672
1750
  * template/coordinates/…): a one-line truncated preview,
@@ -1825,6 +1903,10 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
1825
1903
  // Handled by Tabulator's built-in "color" formatter (see colDef above)
1826
1904
  break;
1827
1905
 
1906
+ case "icon":
1907
+ // Handled by icon_formatter() (see colDef above)
1908
+ break;
1909
+
1828
1910
  case "image":
1829
1911
  // Handled by Tabulator's built-in "image" formatter (see colDef above)
1830
1912
  break;
@@ -32,6 +32,7 @@ import {
32
32
  gobj_short_name,
33
33
  gobj_read_str_attr,
34
34
  gobj_read_bool_attr,
35
+ gobj_read_integer_attr,
35
36
  gobj_start,
36
37
  gobj_stop,
37
38
  gobj_is_running,
@@ -75,7 +76,8 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of o
75
76
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
76
77
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
77
78
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
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_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. ⚠️ DO NOT TURN THIS ON: a partial topic BREAKS LINKING -- the link picker of a record is built from the rows the PARENT topic's table holds, which with paging is one page, so the right parent is usually not offered and an existing link the form cannot match is dropped on save. Off in every consumer until linking asks the backend instead of the sibling table"),
80
+ SDATA(data_type_t.DTP_INTEGER, "page_size", 0, 0, "Rows per page of each topic table (the DISPLAY; the fetch is whole). 0 leaves the table's own default"),
79
81
  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\""),
80
82
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
81
83
  SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
@@ -1041,6 +1043,7 @@ function process_treedb_descs(gobj)
1041
1043
  topic_name: key,
1042
1044
  desc: desc,
1043
1045
  with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging"),
1046
+ page_size: gobj_read_integer_attr(gobj, "page_size"),
1044
1047
  with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar")
1045
1048
  };
1046
1049
 
package/src/lib_icons.js CHANGED
@@ -168,3 +168,54 @@ export function inject_svg_icons()
168
168
  div.innerHTML = ICONS;
169
169
  document.body.appendChild(div);
170
170
  }
171
+
172
+ /*
173
+ * Is `yi-<name>` a real glyph of the loaded set?
174
+ *
175
+ * It has to be ASKED, not looked up in a list: the set is a CSS file
176
+ * (`yui_icons.css`) that an app may extend or replace, and a list here
177
+ * would be a second copy of it, wrong the first time somebody adds a
178
+ * mask rule. And the question matters because the catch-all rule
179
+ * `[class^="yi-"]::before` paints a `currentColor` box for ANY name --
180
+ * so an undefined icon does not fail, it renders as a solid black
181
+ * square, which reads as a glyph nobody recognises rather than as a
182
+ * mistake.
183
+ *
184
+ * The probe is the real stylesheet: a detached element gets no styles,
185
+ * so it is attached, hidden, measured and removed. Cached by name --
186
+ * a table draws one icon per row and the answer cannot change without
187
+ * a reload.
188
+ */
189
+ const __icon_defined__ = {};
190
+
191
+ export function yui_icon_is_defined(name)
192
+ {
193
+ if(!name || typeof name !== "string") {
194
+ return false;
195
+ }
196
+ if(__icon_defined__[name] !== undefined) {
197
+ return __icon_defined__[name];
198
+ }
199
+
200
+ let defined = false;
201
+ try {
202
+ const el = document.createElement("i");
203
+ el.className = name;
204
+ el.style.position = "absolute";
205
+ el.style.visibility = "hidden";
206
+ el.style.pointerEvents = "none";
207
+ document.body.appendChild(el);
208
+ const st = window.getComputedStyle(el, "::before");
209
+ const mask = st.maskImage || st.webkitMaskImage || "";
210
+ defined = !!mask && mask !== "none";
211
+ document.body.removeChild(el);
212
+ } catch(e) {
213
+ /* No DOM to ask (jsdom without the stylesheet, a test): answer
214
+ * "not defined" so the caller falls back to showing the name,
215
+ * which is the honest half of the pair. */
216
+ defined = false;
217
+ }
218
+
219
+ __icon_defined__[name] = defined;
220
+ return defined;
221
+ }