@yuneta/gobj-ui 7.23.59 → 7.23.62

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/index.js CHANGED
@@ -120,6 +120,10 @@ export { addClasses, removeClasses, toggleClasses, removeChildElements, disableE
120
120
  export { inject_svg_icons } from "./src/lib_icons.js";
121
121
  export { EditControl, MarkerControl } from "./src/lib_maplibre.js";
122
122
  export { yui_asset_id, yui_asset_ids, yui_asset_src, yui_asset_element, yui_asset_missing } from "./src/yui_asset.js";
123
+ export {
124
+ YUI_FILE_ACCEPT, yui_file_size_label, yui_file_sha256, yui_array_buffer_to_base64,
125
+ yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_control
126
+ } from "./src/yui_file_field.js";
123
127
  export { yui_toolbar } from "./src/yui_toolbar.js";
124
128
  export { yui_theme_now, yui_is_dark, yui_watch_theme } from "./src/yui_theme.js";
125
129
  export { attach_clear, refresh_clear } from "./src/yui_inputs.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.59",
3
+ "version": "7.23.62",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_form.js CHANGED
@@ -55,6 +55,13 @@ import TomSelect from "tom-select"; // Import Tom-Select JS
55
55
 
56
56
  import { createJSONEditor } from 'vanilla-jsoneditor';
57
57
  import "vanilla-jsoneditor/themes/jse-theme-dark.css";
58
+ import {
59
+ yui_file_control
60
+ } from "./yui_file_field.js";
61
+ import {
62
+ yui_asset_id
63
+ } from "./yui_asset.js";
64
+
58
65
  import "./c_yui_form.css";
59
66
  import {attach_clear, refresh_clear} from "./yui_inputs.js";
60
67
  import {plan_toolbar, DEFAULT_TOOLBAR} from "./form_toolbar_plan.js";
@@ -669,6 +676,13 @@ function build_form_field_conf(gobj, field_desc)
669
676
  field_conf.options = get_fkey_select_options(gobj, field_desc);
670
677
  break;
671
678
 
679
+ /* An fkey into __assets__, and NOT a select: what a person picks
680
+ * is a file on their machine, not a row of an index they have
681
+ * never seen. The bytes travel BESIDE the record (yui_file_field). */
682
+ case "file":
683
+ field_conf.tag = "file";
684
+ break;
685
+
672
686
  case "image":
673
687
  case "icon":
674
688
  case "string":
@@ -1446,6 +1460,24 @@ function create_form_field(
1446
1460
  break;
1447
1461
  }
1448
1462
 
1463
+ case "file":
1464
+ {
1465
+ /* The picked File is KEPT on the element and never enters the
1466
+ * record: a kw is plain json and a File is a host object, so
1467
+ * what the record carries is the id (yui_file_field). */
1468
+ let file_control = yui_file_control(gobj, {
1469
+ name: name,
1470
+ value: "", // set_form_values() fills it from the record
1471
+ readonly: readonly,
1472
+ on_pick: function() {
1473
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
1474
+ }
1475
+ });
1476
+ $extend = file_control.$control;
1477
+ $control.appendChild($extend);
1478
+ break;
1479
+ }
1480
+
1449
1481
  default:
1450
1482
  $extend = createElement2(['strong', {class: 'has-text-danger'}, `Unsupported element type: "${tag}"`]);
1451
1483
  $control.appendChild($extend);
@@ -1546,6 +1578,17 @@ function apply_readonly_to_control(readonly, tag, inputType, $extend)
1546
1578
  * `input` has done since it existed. */
1547
1579
  $extend.classList.add('yui-jse-readonly');
1548
1580
  break;
1581
+ case 'file':
1582
+ /* The button is hidden by the control itself; this is the
1583
+ * INPUT, which stays in the DOM and would otherwise report a
1584
+ * read-only form as writable. */
1585
+ {
1586
+ const $picker = $extend.querySelector('.FILE_INPUT');
1587
+ if($picker) {
1588
+ $picker.disabled = true;
1589
+ }
1590
+ }
1591
+ break;
1549
1592
  case 'input':
1550
1593
  /* `readonly` does not apply to a colour input -- the spec lists
1551
1594
  * the types it applies to and colour is not one of them -- so
@@ -2222,6 +2265,13 @@ function get_form_values(gobj, $form)
2222
2265
  value = getInputValue($input);
2223
2266
  break;
2224
2267
 
2268
+ /* The id the column keeps, never the bytes. A file picked
2269
+ * and not yet saved has no id -- the host reads the File
2270
+ * itself, through `get_picked_files`, and builds __files__. */
2271
+ case "file":
2272
+ value = $input.yui_file_value || "";
2273
+ break;
2274
+
2225
2275
  case "jsoneditor":
2226
2276
  let jsoneditor = $input.jsoneditor;
2227
2277
  if(jsoneditor) {
@@ -2323,6 +2373,20 @@ function clear_data(gobj, $form)
2323
2373
  }
2324
2374
  });
2325
2375
 
2376
+ /* A file control is a `div`, so the input/select/textarea sweep above
2377
+ * does not reach it, and Clear used to leave the old asset showing. */
2378
+ $form.querySelectorAll('.FILE_FIELD').forEach($file => {
2379
+ $file.yui_file = null;
2380
+ $file.yui_file_value = "";
2381
+ let $picker = $file.querySelector('.FILE_INPUT');
2382
+ if($picker) {
2383
+ $picker.value = "";
2384
+ }
2385
+ if($file.yui_file_render) {
2386
+ $file.yui_file_render();
2387
+ }
2388
+ });
2389
+
2326
2390
  $form.querySelectorAll('.yui-tabulator-table').forEach($table => {
2327
2391
  if($table.tabulator.initialized) {
2328
2392
  $table.tabulator.clearData();
@@ -2427,6 +2491,18 @@ function set_form_values(gobj, template, $form, record)
2427
2491
  }
2428
2492
 
2429
2493
  switch(tag) {
2494
+ /* A `file` column comes back as an fkey, in any of the three
2495
+ * shapes a link takes -- the stored reference, the bare id
2496
+ * `fkey_only_id` collapses it to, or an expanded ref -- so it
2497
+ * is read with the reader that knows all three. */
2498
+ case "file":
2499
+ $input.yui_file = null;
2500
+ $input.yui_file_value = yui_asset_id(value) || "";
2501
+ if($input.yui_file_render) {
2502
+ $input.yui_file_render();
2503
+ }
2504
+ break;
2505
+
2430
2506
  case "input":
2431
2507
  switch(input_type) {
2432
2508
  case "datetime-local":
@@ -2589,6 +2665,14 @@ function treedb_value_2_form_value(gobj, field_desc, value)
2589
2665
  }
2590
2666
 
2591
2667
  switch(type) {
2668
+ /* A `file` column is handed to the control AS IT COMES: the
2669
+ * control reads the fkey itself (yui_asset_id knows the three
2670
+ * shapes a link comes back in), and converting it here would be
2671
+ * reading it twice, in two places, with two ideas of what a link
2672
+ * looks like. */
2673
+ case "file":
2674
+ return value;
2675
+
2592
2676
  case "fkey": {
2593
2677
  /* Decode ref(s) ("topic^id^hook", bare "$id", dict keys or
2594
2678
  * {topic_name,id,hook_name}) into a plain array of ids for
@@ -2739,6 +2823,13 @@ function form_value_2_treedb_value(gobj, field_desc, value)
2739
2823
  }
2740
2824
 
2741
2825
  switch(type) {
2826
+ /* The BARE id, and treedb expands it into the full reference
2827
+ * (`__assets__^<sha>^as_<T>_<C>`). That is the whole point of the
2828
+ * host declaring nothing but the column: the client never has to
2829
+ * learn the name of a hook that is DERIVED. */
2830
+ case "file":
2831
+ break;
2832
+
2742
2833
  case "fkey":
2743
2834
  value = build_fkey_ref(gobj, field_desc, value);
2744
2835
  break;
@@ -3123,14 +3214,79 @@ function ac_timeout(gobj, event, kw, src)
3123
3214
 
3124
3215
 
3125
3216
 
3217
+ /************************************************************
3218
+ * Framework Method command
3219
+ ************************************************************/
3220
+ function mt_command_parser(gobj, command, kw, src)
3221
+ {
3222
+ switch(command) {
3223
+ case "get_picked_files":
3224
+ return cmd_get_picked_files(gobj, command, kw, src);
3225
+ default:
3226
+ log_error(`${gobj_short_name(gobj)}: command not found: ${command}`);
3227
+ return {
3228
+ result: -1,
3229
+ comment: `Command not found: ${command}`,
3230
+ schema: null,
3231
+ data: null
3232
+ };
3233
+ }
3234
+ }
3235
+
3236
+ /************************************************************
3237
+ * The files a person picked and the record cannot carry.
3238
+ *
3239
+ * A `File` is a host object and a kw is plain json -- the machine trace
3240
+ * serialises it -- so the picked files never travel in an event. The
3241
+ * form KEEPS them and the HOST asks for them here, at save, and turns
3242
+ * them into the `__files__` manifest that rides beside the record.
3243
+ *
3244
+ * Answers `data: {col: File}`, empty when nothing was picked, which is
3245
+ * the ordinary case and not an error.
3246
+ ************************************************************/
3247
+ function cmd_get_picked_files(gobj, cmd, kw, src)
3248
+ {
3249
+ const files = {};
3250
+ const $container = gobj_read_attr(gobj, "$container");
3251
+ const $form = $container? $container.querySelector('form'): null;
3252
+
3253
+ if(!$form) {
3254
+ log_error(`${gobj_short_name(gobj)}: no form to read the picked files from`);
3255
+ return {
3256
+ result: -1,
3257
+ comment: "no form",
3258
+ schema: null,
3259
+ data: null
3260
+ };
3261
+ }
3262
+
3263
+ $form.querySelectorAll('.yui-form-data-input').forEach(($input) => {
3264
+ if($input.dataset.form_tag !== "file") {
3265
+ return;
3266
+ }
3267
+ if($input.yui_file) {
3268
+ files[$input.dataset.treedb_name] = $input.yui_file;
3269
+ }
3270
+ });
3271
+
3272
+ return {
3273
+ result: 0,
3274
+ comment: "",
3275
+ schema: null,
3276
+ data: files
3277
+ };
3278
+ }
3279
+
3280
+
3126
3281
  /*---------------------------------------------*
3127
3282
  * Global methods table
3128
3283
  *---------------------------------------------*/
3129
3284
  const gmt = {
3130
- mt_create: mt_create,
3131
- mt_start: mt_start,
3132
- mt_stop: mt_stop,
3133
- mt_destroy: mt_destroy
3285
+ mt_create: mt_create,
3286
+ mt_start: mt_start,
3287
+ mt_stop: mt_stop,
3288
+ mt_destroy: mt_destroy,
3289
+ mt_command_parser: mt_command_parser
3134
3290
  };
3135
3291
 
3136
3292
  /***************************************************************
@@ -126,3 +126,18 @@
126
126
  line-height: 1;
127
127
  vertical-align: middle;
128
128
  }
129
+
130
+ /*
131
+ * What a `file` column shows in a cell: the asset id, shortened, or the
132
+ * word for having none. Monospace because it is a hash and a hash read
133
+ * in a proportional font is unreadable.
134
+ */
135
+ .FILE_CELL {
136
+ font-family: monospace;
137
+ }
138
+
139
+ .FILE_CELL.is-empty {
140
+ font-family: inherit;
141
+ font-style: italic;
142
+ opacity: 0.6;
143
+ }
@@ -92,6 +92,8 @@ import {row_matches} from "./yui_row_search.js";
92
92
  import {t} from "i18next";
93
93
 
94
94
  import {plan_treedb_writes, READONLY_FORM_TOOLBAR} from "./treedb_write_plan.js";
95
+ import {yui_file_read, yui_files_manifest, yui_file_id_label} from "./yui_file_field.js";
96
+ import {yui_asset_id} from "./yui_asset.js";
95
97
  import {delete_impact} from "./delete_impact.js";
96
98
 
97
99
  import "./c_yui_treedb_topic_with_form.css";
@@ -1293,6 +1295,14 @@ function create_tabulator(gobj)
1293
1295
  vertAlign = "middle";
1294
1296
  colFormatter = icon_formatter;
1295
1297
  break;
1298
+ /* An fkey into __assets__. It draws the id and NOT the
1299
+ * picture, because a picture is a `get-asset` per cell -- an
1300
+ * action, with its own event and its own answer -- and a
1301
+ * table of 50 rows would fire 50 of them at render time. The
1302
+ * id is what the cell can say honestly on its own. */
1303
+ case "file":
1304
+ colFormatter = file_cell_formatter;
1305
+ break;
1296
1306
  case "object":
1297
1307
  case "dict":
1298
1308
  case "template":
@@ -1743,6 +1753,28 @@ function table__destroy(gobj)
1743
1753
  * render a solid square -- a glyph nobody recognises rather
1744
1754
  * than a mistake somebody can fix.
1745
1755
  ************************************************************/
1756
+ /************************************************************
1757
+ * What a `file` column shows in a table cell: the asset it names.
1758
+ *
1759
+ * A sha256 in full is 64 characters of noise in a column, so it is
1760
+ * shortened and the whole of it is the tooltip. Empty is SAID, not left
1761
+ * blank -- a blank cell and a cell whose asset failed to load look the
1762
+ * same, and only one of them is normal.
1763
+ ************************************************************/
1764
+ function file_cell_formatter(cell, formatterParams, onRendered)
1765
+ {
1766
+ const id = yui_asset_id(cell.getValue());
1767
+ if(!id) {
1768
+ const $empty = createElement2(
1769
+ ["span", {class: "FILE_CELL is-empty", "data-i18n": "no file"}, "no file"]
1770
+ );
1771
+ return $empty;
1772
+ }
1773
+ return createElement2(
1774
+ ["span", {class: "FILE_CELL", title: id}, yui_file_id_label(id)]
1775
+ );
1776
+ }
1777
+
1746
1778
  function icon_formatter(cell)
1747
1779
  {
1748
1780
  let name = cell.getValue();
@@ -1869,6 +1901,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
1869
1901
  }
1870
1902
  break;
1871
1903
 
1904
+ case "file": // an fkey into __assets__, read like any other
1872
1905
  case "fkey": // Convert data from backend to frontend TABLE CELL
1873
1906
  let new_value = [];
1874
1907
  if(value) {
@@ -2639,6 +2672,12 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation)
2639
2672
  value = null;
2640
2673
  break;
2641
2674
 
2675
+ /* A `file` column is written by the FORM, which sends the bare
2676
+ * id and lets treedb expand it (the client never learns the
2677
+ * derived hook name). In the table it is not editable in place. */
2678
+ case "file":
2679
+ break;
2680
+
2642
2681
  case "fkey": // Convert data from frontend to backend
2643
2682
  value = build_fkey_ref(gobj, col, value);
2644
2683
  break;
@@ -3430,22 +3469,110 @@ function ac_form_save_record(gobj, event, kw, src)
3430
3469
  return -1; /* Error already logged */
3431
3470
  }
3432
3471
  let mode = gobj_read_str_attr(src, "form_mode");
3472
+
3473
+ /*------------------------------------------------------*
3474
+ * Did anybody pick a file?
3475
+ *
3476
+ * A `File` is a host object and a kw is plain json, so the picked
3477
+ * files never came with the record: the form KEPT them and this is
3478
+ * where they are asked for. Nothing picked is the ordinary case.
3479
+ *------------------------------------------------------*/
3480
+ let picked = {};
3481
+ const answer = gobj_command(src, "get_picked_files", {}, gobj);
3482
+ if(answer && answer.result >= 0 && answer.data) {
3483
+ picked = answer.data;
3484
+ }
3485
+ const cols = Object.keys(picked);
3486
+
3487
+ if(cols.length === 0) {
3488
+ publish_treedb_write(gobj, mode, kw);
3489
+
3490
+ /* we are INSIDE the form's gobj_publish_event stack — never
3491
+ * destroy the publisher synchronously from a subscriber
3492
+ * callback: defer the close */
3493
+ setTimeout(function() {
3494
+ close_form_dialog(gobj);
3495
+ }, 0);
3496
+ return 0;
3497
+ }
3498
+
3499
+ /*------------------------------------------------------*
3500
+ * Reading a file is a PROMISE, and a resolved promise is an OS
3501
+ * notification: it enters the machine as an EVENT, never as a
3502
+ * chain of callbacks that the trace cannot see. The dialog stays
3503
+ * open until it lands, which is what tells the person that 40 MB
3504
+ * are still going up.
3505
+ *------------------------------------------------------*/
3506
+ Promise.all(cols.map((col) => {
3507
+ return yui_file_read(picked[col]).then((read) => {
3508
+ return {col: col, read: read};
3509
+ });
3510
+ })).then((reads) => {
3511
+ const picks = {};
3512
+ reads.forEach((r) => {
3513
+ picks[r.col] = r.read;
3514
+ });
3515
+ gobj_send_event(gobj, "EV_FILES_READ",
3516
+ {mode: mode, record: kw, picks: picks}, gobj
3517
+ );
3518
+ }).catch((e) => {
3519
+ gobj_send_event(gobj, "EV_FILES_FAILED",
3520
+ {reason: (e && e.message)? e.message: String(e)}, gobj
3521
+ );
3522
+ });
3523
+
3524
+ return 0;
3525
+ }
3526
+
3527
+ /************************************************************
3528
+ * The one place a write of this view leaves for the app.
3529
+ ************************************************************/
3530
+ function publish_treedb_write(gobj, mode, record, jn_files)
3531
+ {
3532
+ const kw_write = {
3533
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
3534
+ record: record
3535
+ };
3536
+ if(jn_files && Object.keys(jn_files).length > 0) {
3537
+ /* Not a column: an instruction to the treedb write path,
3538
+ * consumed and dropped at the door. */
3539
+ kw_write.record.__files__ = jn_files;
3540
+ }
3433
3541
  gobj_publish_event(
3434
3542
  gobj,
3435
3543
  (mode === "create")? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD",
3436
- {
3437
- topic_name: gobj_read_str_attr(gobj, "topic_name"),
3438
- record: kw
3439
- }
3544
+ kw_write
3440
3545
  );
3546
+ }
3441
3547
 
3442
- /* we are INSIDE the form's gobj_publish_event stack — never
3443
- * destroy the publisher synchronously from a subscriber
3444
- * callback: defer the close */
3445
- setTimeout(function() {
3446
- close_form_dialog(gobj);
3447
- }, 0);
3548
+ /************************************************************
3549
+ * {mode, record, picks} -- the bytes are here now
3550
+ ************************************************************/
3551
+ function ac_files_read(gobj, event, kw, src)
3552
+ {
3553
+ const {record, __files__} = yui_files_manifest(kw.picks, kw.record);
3554
+ publish_treedb_write(gobj, kw.mode, record, __files__);
3555
+ close_form_dialog(gobj);
3556
+ return 0;
3557
+ }
3448
3558
 
3559
+ /************************************************************
3560
+ * {reason} -- the browser could not read what was picked
3561
+ *
3562
+ * The form stays OPEN: the record is still there, and closing it would
3563
+ * throw away what the person typed for a failure that is not theirs.
3564
+ ************************************************************/
3565
+ function ac_files_failed(gobj, event, kw, src)
3566
+ {
3567
+ log_error(`${gobj_short_name(gobj)}: cannot read the picked file: ${kw.reason}`);
3568
+ /* An i18n KEY, not a sentence: the dialog renders it with `i18n`, so
3569
+ * a sentence here would be a key nobody defined and would render as
3570
+ * itself in every language. The reason is DATA and stays in the log,
3571
+ * where it can be read without being translated. */
3572
+ yui_shell_confirm_ok(
3573
+ yui_shell_of(gobj), 'cannot read the picked file',
3574
+ {t: t, ok_label: "accept"}
3575
+ );
3449
3576
  return 0;
3450
3577
  }
3451
3578
 
@@ -4041,6 +4168,8 @@ function create_gclass(gclass_name)
4041
4168
 
4042
4169
  ["EV_EDITION_MODE", ac_edition_mode, null],
4043
4170
  ["EV_SAVE_RECORD", ac_form_save_record, null],
4171
+ ["EV_FILES_READ", ac_files_read, null],
4172
+ ["EV_FILES_FAILED", ac_files_failed, null],
4044
4173
 
4045
4174
  ["EV_NEW_ROW", ac_new_row, null],
4046
4175
  ["EV_DELETE_ROWS", ac_delete_rows, null],
@@ -4082,6 +4211,8 @@ function create_gclass(gclass_name)
4082
4211
 
4083
4212
  ["EV_EDITION_MODE", 0],
4084
4213
  ["EV_SAVE_RECORD", 0],
4214
+ ["EV_FILES_READ", 0],
4215
+ ["EV_FILES_FAILED", 0],
4085
4216
  ["EV_NEW_ROW", 0],
4086
4217
  ["EV_DELETE_ROWS", 0],
4087
4218
  ["EV_COPY_ROWS", 0],
@@ -0,0 +1,67 @@
1
+ /*
2
+ * yui_file_field.css
3
+ *
4
+ * The control for a treedb `file` column. One row: the button, what it
5
+ * is holding, and the way to clear it.
6
+ *
7
+ * No transitions -- the state changes the moment the person acts on it.
8
+ *
9
+ * Copyright (c) 2026, ArtGins.
10
+ * All Rights Reserved.
11
+ */
12
+
13
+ .FILE_FIELD {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 0.5rem;
17
+ flex-wrap: wrap;
18
+ min-width: 0;
19
+ }
20
+
21
+ .FILE_FIELD .FILE_PICK,
22
+ .FILE_FIELD .FILE_CLEAR {
23
+ flex-shrink: 0;
24
+ transition: none;
25
+ }
26
+
27
+ .FILE_FIELD .FILE_PICK_ICON,
28
+ .FILE_FIELD .FILE_CLEAR_ICON {
29
+ margin-right: 0.25rem;
30
+ }
31
+
32
+ .FILE_FIELD .FILE_CLEAR_ICON {
33
+ margin-right: 0;
34
+ }
35
+
36
+ /*
37
+ * The name is the part that must give way: an id or a long filename
38
+ * cannot push the buttons out of the row.
39
+ */
40
+ .FILE_FIELD .FILE_NAME {
41
+ flex: 1 1 8rem;
42
+ min-width: 0;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ white-space: nowrap;
46
+ font-family: monospace;
47
+ }
48
+
49
+ .FILE_FIELD .FILE_SIZE {
50
+ flex-shrink: 0;
51
+ opacity: 0.7;
52
+ font-size: 0.85em;
53
+ }
54
+
55
+ .FILE_FIELD .FILE_STATE {
56
+ flex-shrink: 0;
57
+ font-size: 0.85em;
58
+ }
59
+
60
+ .FILE_FIELD .FILE_STATE.is-picked {
61
+ font-weight: bold;
62
+ }
63
+
64
+ .FILE_FIELD .FILE_STATE.is-empty {
65
+ opacity: 0.6;
66
+ font-style: italic;
67
+ }