@yuneta/gobj-ui 7.23.158 → 7.23.159

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.158",
3
+ "version": "7.23.159",
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
@@ -61,7 +61,8 @@ import {
61
61
  yui_file_control
62
62
  } from "./yui_file_field.js";
63
63
  import {
64
- yui_asset_id
64
+ yui_asset_id,
65
+ yui_asset_release
65
66
  } from "./yui_asset.js";
66
67
 
67
68
  import "./c_yui_form.css";
@@ -192,6 +193,9 @@ function mt_create(gobj)
192
193
  ************************************************************/
193
194
  function mt_destroy(gobj)
194
195
  {
196
+ /* A file preview holds blob urls, which keep their bytes alive
197
+ * until revoked -- and removing the DOM revokes nothing. */
198
+ yui_asset_release(gobj_read_attr(gobj, "$container"));
195
199
  destroy_ui(gobj);
196
200
  }
197
201
 
@@ -3155,6 +3159,51 @@ function ac_clear_record(gobj, event, kw, src)
3155
3159
  return 0;
3156
3160
  }
3157
3161
 
3162
+ /************************************************************
3163
+ * From the host: what it fetched for the asset a `file` column
3164
+ * names -- the `get-asset` answer, or why there is none.
3165
+ * {
3166
+ * name: the column
3167
+ * id: the asset id the answer is for
3168
+ * answer: the get-asset answer, or null
3169
+ * error: why there is no answer, when there is none
3170
+ * }
3171
+ *
3172
+ * Kept on the control and drawn only while the column still names
3173
+ * THAT id: an answer landing after the person picked another file
3174
+ * or cleared the column must not bring the old picture back.
3175
+ ************************************************************/
3176
+ function ac_set_file_preview(gobj, event, kw, src)
3177
+ {
3178
+ let $container = gobj_read_attr(gobj, "$container");
3179
+ let $form = $container? $container.querySelector('form'): null;
3180
+ if(!$form) {
3181
+ log_error(`${gobj_short_name(gobj)}: no form to show the file preview in`);
3182
+ return -1;
3183
+ }
3184
+
3185
+ let $file = null;
3186
+ $form.querySelectorAll('.FILE_FIELD').forEach(($el) => {
3187
+ if($el.dataset.treedb_name === kw.name) {
3188
+ $file = $el;
3189
+ }
3190
+ });
3191
+ if(!$file) {
3192
+ log_error(`${gobj_short_name(gobj)}: no file column '${kw.name}' in the form`);
3193
+ return -1;
3194
+ }
3195
+
3196
+ $file.yui_file_stored = {
3197
+ id: kw.id || "",
3198
+ answer: kw.answer || null,
3199
+ error: kw.error || ""
3200
+ };
3201
+ if($file.yui_file_render) {
3202
+ $file.yui_file_render();
3203
+ }
3204
+ return 0;
3205
+ }
3206
+
3158
3207
  /************************************************************
3159
3208
  * From internal form button for new rows
3160
3209
  * or from load_tabulator_data() to load data from backend
@@ -3390,6 +3439,7 @@ function create_gclass(gclass_name)
3390
3439
  ["EV_RECORD_CHANGED", ac_record_changed, null],
3391
3440
  ["EV_COPY_RECORD", ac_copy_record, null],
3392
3441
  ["EV_PASTE_RECORD", ac_paste_record, null],
3442
+ ["EV_SET_FILE_PREVIEW", ac_set_file_preview, null],
3393
3443
 
3394
3444
  ["EV_WINDOW_MOVED", null, null],
3395
3445
  ["EV_WINDOW_RESIZED", null, null],
@@ -3413,6 +3463,7 @@ function create_gclass(gclass_name)
3413
3463
  ["EV_RECORD_CHANGED", 0],
3414
3464
  ["EV_COPY_RECORD", 0],
3415
3465
  ["EV_PASTE_RECORD", 0],
3466
+ ["EV_SET_FILE_PREVIEW", 0],
3416
3467
 
3417
3468
  ["EV_WINDOW_MOVED", 0],
3418
3469
  ["EV_WINDOW_RESIZED", 0],
@@ -141,3 +141,70 @@
141
141
  font-style: italic;
142
142
  opacity: 0.6;
143
143
  }
144
+
145
+ /*
146
+ * A file cell that names an asset is a LINK: the click opens it
147
+ * (EV_SHOW_CELL_FILE). Same shape as the JSON cell beside it.
148
+ */
149
+ .FILE_CELL_LINK {
150
+ white-space: nowrap;
151
+ }
152
+ .FILE_CELL_LINK .FILE_CELL_ICON {
153
+ margin-right: 0.25rem;
154
+ vertical-align: middle;
155
+ }
156
+ .FILE_CELL_LINK .FILE_CELL_ID {
157
+ text-decoration: underline;
158
+ vertical-align: middle;
159
+ }
160
+ .FILE_CELL_LINK .FILE_CELL_MORE {
161
+ margin-left: 0.35rem;
162
+ font-family: inherit;
163
+ opacity: 0.7;
164
+ vertical-align: middle;
165
+ }
166
+
167
+ /*
168
+ * The file popup: one item per asset the cell names, each the thing
169
+ * itself and a caption (what it is, and a way to open it full size).
170
+ */
171
+ .TREEDB_CELL_FILE {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 1rem;
175
+ padding: 0.75rem;
176
+ height: 100%;
177
+ overflow: auto;
178
+ }
179
+ .TREEDB_CELL_FILE .CELL_FILE_ITEM {
180
+ display: flex;
181
+ flex-direction: column;
182
+ align-items: center;
183
+ gap: 0.35rem;
184
+ min-width: 0;
185
+ }
186
+ .TREEDB_CELL_FILE img.CELL_FILE_MEDIA,
187
+ .TREEDB_CELL_FILE video.CELL_FILE_MEDIA {
188
+ display: block;
189
+ max-width: 100%;
190
+ max-height: 70vh;
191
+ object-fit: contain;
192
+ }
193
+ .TREEDB_CELL_FILE audio.CELL_FILE_MEDIA {
194
+ width: 100%;
195
+ }
196
+ .TREEDB_CELL_FILE iframe.CELL_FILE_MEDIA {
197
+ min-height: 60vh;
198
+ }
199
+ .TREEDB_CELL_FILE .CELL_FILE_CAPTION {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ justify-content: center;
203
+ gap: 0.75rem;
204
+ font-size: 0.8rem;
205
+ opacity: 0.85;
206
+ }
207
+ .TREEDB_CELL_FILE .CELL_FILE_ID {
208
+ font-family: monospace;
209
+ overflow-wrap: anywhere;
210
+ }
@@ -96,8 +96,12 @@ import {t} from "i18next";
96
96
  import {yui_toolbar_icon} from "./yui_toolbar.js";
97
97
 
98
98
  import {plan_treedb_writes, READONLY_FORM_TOOLBAR} from "./treedb_write_plan.js";
99
- import {yui_file_read, yui_files_manifest, yui_file_id_label} from "./yui_file_field.js";
100
- import {yui_asset_id} from "./yui_asset.js";
99
+ import {
100
+ yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
101
+ } from "./yui_file_field.js";
102
+ import {
103
+ yui_asset_ids, yui_asset_element, yui_asset_open_link, yui_asset_release
104
+ } from "./yui_asset.js";
101
105
  import {delete_impact} from "./delete_impact.js";
102
106
 
103
107
  import "./c_yui_treedb_topic_with_form.css";
@@ -249,6 +253,11 @@ let PRIVATE_DATA = {
249
253
  cell_json_gobj: null, // hosted C_YUI_JSON child (while a cell is open)
250
254
  cell_json_modal: null, // { close } handle of the cell dialog (phone)
251
255
  cell_json_win: null, // C_YUI_WINDOW presenting it (laptop)
256
+ cell_file_box: null, // $box of the file popup (while a file cell is open)
257
+ cell_file_modal: null, // { close } handle of the file dialog (phone)
258
+ cell_file_win: null, // C_YUI_WINDOW presenting it (laptop)
259
+ _pending_assets: null, // req_id -> {target, col, id, slot, timer}
260
+ _asset_seq: 0, // correlation id of an asset request
252
261
  };
253
262
 
254
263
  let __gclass__ = null;
@@ -449,6 +458,8 @@ function mt_stop(gobj)
449
458
  close_form_dialog(gobj);
450
459
  close_schema_dialog(gobj);
451
460
  close_cell_json_dialog(gobj);
461
+ close_cell_file_dialog(gobj);
462
+ drop_pending_assets(gobj);
452
463
  table__destroy(gobj);
453
464
  }
454
465
 
@@ -464,6 +475,8 @@ function mt_destroy(gobj)
464
475
  close_form_dialog(gobj);
465
476
  close_schema_dialog(gobj);
466
477
  close_cell_json_dialog(gobj);
478
+ close_cell_file_dialog(gobj);
479
+ drop_pending_assets(gobj);
467
480
  destroy_ui(gobj);
468
481
  }
469
482
 
@@ -1309,9 +1322,31 @@ function create_tabulator(gobj)
1309
1322
  * picture, because a picture is a `get-asset` per cell -- an
1310
1323
  * action, with its own event and its own answer -- and a
1311
1324
  * table of 50 rows would fire 50 of them at render time. The
1312
- * id is what the cell can say honestly on its own. */
1325
+ * id is what the cell can say honestly on its own; a CLICK
1326
+ * asks for the bytes and shows them (EV_SHOW_CELL_FILE). */
1313
1327
  case "file":
1314
1328
  colFormatter = file_cell_formatter;
1329
+ cellClick = function(e, cell) {
1330
+ if(!e.target.closest('.FILE_CELL_LINK')) {
1331
+ return; /* an empty cell: nothing to show */
1332
+ }
1333
+ e.stopPropagation();
1334
+ let pkey = desc.pkey || "id";
1335
+ let row_id = cell.getData()[pkey];
1336
+ if(row_id === undefined || row_id === null) {
1337
+ log_error(
1338
+ `${gobj_short_name(gobj)}: row without pkey '${pkey}',` +
1339
+ ` cannot open the file of '${cell.getField()}'`
1340
+ );
1341
+ return;
1342
+ }
1343
+ gobj_send_event(
1344
+ gobj,
1345
+ "EV_SHOW_CELL_FILE",
1346
+ {row_id: row_id, col_id: cell.getField()},
1347
+ gobj
1348
+ );
1349
+ };
1315
1350
  break;
1316
1351
  case "object":
1317
1352
  case "dict":
@@ -1835,21 +1870,51 @@ function table__destroy(gobj)
1835
1870
  * What a `file` column shows in a table cell: the asset it names.
1836
1871
  *
1837
1872
  * A sha256 in full is 64 characters of noise in a column, so it is
1838
- * shortened and the whole of it is the tooltip. Empty is SAID, not left
1839
- * blank -- a blank cell and a cell whose asset failed to load look the
1840
- * same, and only one of them is normal.
1873
+ * shortened and the whole of it goes to the popup the click opens. Empty
1874
+ * is SAID, not left blank -- a blank cell and a cell whose asset failed
1875
+ * to load look the same, and only one of them is normal.
1876
+ *
1877
+ * The value arrives already turned into ids joined by ", "
1878
+ * (transform__treedb_value_2_table_value), and an array column names
1879
+ * several: the first is shown and the rest counted.
1841
1880
  ************************************************************/
1881
+ function file_cell_ids(value)
1882
+ {
1883
+ if(is_array(value)) {
1884
+ return yui_asset_ids(value);
1885
+ }
1886
+ return String(value || "").split(",")
1887
+ .map((s) => s.trim())
1888
+ .filter((s) => !!s)
1889
+ .flatMap((s) => yui_asset_ids(s));
1890
+ }
1891
+
1842
1892
  function file_cell_formatter(cell, formatterParams, onRendered)
1843
1893
  {
1844
- const id = yui_asset_id(cell.getValue());
1845
- if(!id) {
1894
+ const ids = file_cell_ids(cell.getValue());
1895
+ if(!ids.length) {
1846
1896
  const $empty = createElement2(
1847
1897
  ["span", {class: "FILE_CELL is-empty", "data-i18n": "no file"}, "no file"]
1848
1898
  );
1849
1899
  return $empty;
1850
1900
  }
1901
+ const children = [
1902
+ ['span', {class: 'FILE_CELL_ICON icon yi-eye'}],
1903
+ ['span', {class: 'FILE_CELL_ID'}, yui_file_id_label(ids[0])]
1904
+ ];
1905
+ if(ids.length > 1) {
1906
+ children.push(['span', {class: 'FILE_CELL_MORE'}, `+${ids.length - 1}`]);
1907
+ }
1908
+ /* A link and not a button, like the JSON cell beside it; named for
1909
+ * the eye and for a reader, through its key. */
1851
1910
  return createElement2(
1852
- ["span", {class: "FILE_CELL", title: id}, yui_file_id_label(id)]
1911
+ ['a', {
1912
+ class: 'FILE_CELL FILE_CELL_LINK',
1913
+ title: t('show file'),
1914
+ 'data-i18n-title': 'show file',
1915
+ 'aria-label': t('show file'),
1916
+ 'data-i18n-aria-label': 'show file'
1917
+ }, children]
1853
1918
  );
1854
1919
  }
1855
1920
 
@@ -2243,6 +2308,10 @@ function open_form_dialog(gobj, mode, record)
2243
2308
  gobj_start(form); // mt_start loads `record` into the form
2244
2309
  refresh_language($body, t); // translate anything the form emitted on start
2245
2310
 
2311
+ /* The form shows the asset a `file` column names only once the host
2312
+ * has fetched it -- asking the backend is this gclass's action. */
2313
+ request_form_previews(gobj, record);
2314
+
2246
2315
  /* Focus the pkey unless the mode made it readonly (update) —
2247
2316
  * then the first editable field. */
2248
2317
  let $with_focus = $body.querySelector('.with-focus:not([readonly])');
@@ -2573,6 +2642,256 @@ function close_cell_json_dialog(gobj)
2573
2642
  teardown_cell_json_child(gobj);
2574
2643
  }
2575
2644
 
2645
+ /************************************************************
2646
+ * Ask the host for the bytes of ONE asset.
2647
+ *
2648
+ * Same contract as the page requests: the transport belongs to the
2649
+ * HOST, so the request goes up as an event and the answer comes back
2650
+ * down as one, correlated by a `req_id` the host echoes. What is parked
2651
+ * here is WHERE the answer goes -- the form's preview of a column, or a
2652
+ * slot of the file popup -- and the watchdog, because a request whose
2653
+ * answer never lands would leave a "loading" on screen for ever.
2654
+ *
2655
+ * `target` is kept here and never travels: it holds a gobj and a DOM
2656
+ * node, and a kw is plain json.
2657
+ ************************************************************/
2658
+ const ASSET_TIMEOUT_MS = 30000;
2659
+
2660
+ function request_asset(gobj, asset_id, target)
2661
+ {
2662
+ let priv = gobj.priv;
2663
+ if(!priv._pending_assets) {
2664
+ priv._pending_assets = {};
2665
+ }
2666
+ let req_id = `a${++priv._asset_seq}`;
2667
+
2668
+ let timer = window.setTimeout(function() {
2669
+ gobj_send_event(gobj, "EV_ASSET_FAILED",
2670
+ {req_id: req_id, error: "asset request timed out"}, gobj);
2671
+ }, ASSET_TIMEOUT_MS);
2672
+
2673
+ priv._pending_assets[req_id] = Object.assign({id: asset_id, timer: timer}, target);
2674
+
2675
+ gobj_publish_event(
2676
+ gobj,
2677
+ "EV_REQUEST_ASSET",
2678
+ {
2679
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
2680
+ req_id: req_id,
2681
+ asset_id: asset_id
2682
+ }
2683
+ );
2684
+ }
2685
+
2686
+ /************************************************************
2687
+ * Take a parked asset request out, however it ended. Null when
2688
+ * it was already settled (a late answer after a timeout).
2689
+ ************************************************************/
2690
+ function take_pending_asset(gobj, req_id)
2691
+ {
2692
+ let priv = gobj.priv;
2693
+ let pend = priv._pending_assets? priv._pending_assets[req_id] : null;
2694
+ if(!pend) {
2695
+ return null;
2696
+ }
2697
+ delete priv._pending_assets[req_id];
2698
+ if(pend.timer) {
2699
+ window.clearTimeout(pend.timer);
2700
+ }
2701
+ return pend;
2702
+ }
2703
+
2704
+ /************************************************************
2705
+ * Forget every parked asset request: the view is going away,
2706
+ * and a watchdog firing into a stopped gobj is an error for
2707
+ * nothing.
2708
+ ************************************************************/
2709
+ function drop_pending_assets(gobj)
2710
+ {
2711
+ let priv = gobj.priv;
2712
+ if(priv._pending_assets) {
2713
+ for(const pend of Object.values(priv._pending_assets)) {
2714
+ if(pend.timer) {
2715
+ window.clearTimeout(pend.timer);
2716
+ }
2717
+ }
2718
+ }
2719
+ priv._pending_assets = null;
2720
+ }
2721
+
2722
+ /************************************************************
2723
+ * Ask for the asset of every `file` column the record names,
2724
+ * for the form's preview. One per column: the form shows the
2725
+ * asset the column keeps, which is the first it names.
2726
+ ************************************************************/
2727
+ function request_form_previews(gobj, record)
2728
+ {
2729
+ let priv = gobj.priv;
2730
+ if(!record || !priv.form) {
2731
+ return;
2732
+ }
2733
+ let desc = gobj_read_attr(gobj, "desc");
2734
+ for(let col of desc.cols) {
2735
+ if(!col.id || col.id.charAt(0) === '_') {
2736
+ continue;
2737
+ }
2738
+ const field_desc = treedb_get_field_desc(col);
2739
+ if(field_desc.type !== "file" || field_desc.is_hidden) {
2740
+ continue;
2741
+ }
2742
+ let ids = file_cell_ids(record[col.id]);
2743
+ if(!ids.length) {
2744
+ continue; /* a column that names nothing: nothing to fetch */
2745
+ }
2746
+ request_asset(gobj, ids[0], {target: "form", form: priv.form, col: col.id});
2747
+ }
2748
+ }
2749
+
2750
+ /************************************************************
2751
+ * Show what a `file` cell names -- every asset, when the
2752
+ * column is an array -- in the standardized adaptive dialog.
2753
+ *
2754
+ * Unlike the JSON cell this is NOT offline: the row holds the
2755
+ * ids and the bytes are the backend's, so each slot opens as
2756
+ * "loading" and one `get-asset` per id fills it.
2757
+ ************************************************************/
2758
+ function open_cell_file_dialog(gobj, row_id, col_id)
2759
+ {
2760
+ close_cell_file_dialog(gobj); // only one at a time
2761
+
2762
+ let priv = gobj.priv;
2763
+
2764
+ let tabulator = gobj_read_attr(gobj, "tabulator");
2765
+ if(!tabulator) {
2766
+ log_error(`${gobj_short_name(gobj)}: no table, no file cell to show`);
2767
+ return;
2768
+ }
2769
+ let row = tabulator.getRow(row_id);
2770
+ if(!row) {
2771
+ log_error(`${gobj_short_name(gobj)}: row '${row_id}' not found in the table`);
2772
+ return;
2773
+ }
2774
+ let ids = file_cell_ids(row.getData()[col_id]);
2775
+ if(!ids.length) {
2776
+ log_error(`${gobj_short_name(gobj)}: row '${row_id}' col '${col_id}' names no file`);
2777
+ return;
2778
+ }
2779
+
2780
+ /* The id is DATA, shown whole: it is what a person pastes into a
2781
+ * command or compares, and the cell could only show it cut. */
2782
+ let $items = ids.map((id) => createElement2(
2783
+ ['div', {class: 'CELL_FILE_ITEM'}, [
2784
+ ['div', {class: 'CELL_FILE_SLOT'}, [
2785
+ ['span', {class: 'CELL_FILE_LOADING', i18n: 'loading'}, 'loading']
2786
+ ]],
2787
+ ['div', {class: 'CELL_FILE_CAPTION'}, [
2788
+ ['span', {class: 'CELL_FILE_ID'}, id]
2789
+ ]]
2790
+ ]]
2791
+ ));
2792
+ let $box = createElement2(['div', {class: 'TREEDB_CELL_FILE'}, $items]);
2793
+
2794
+ /* Title in two halves, same contract as the JSON cell: the record id
2795
+ * is DATA and the column carries its own i18n key. */
2796
+ let presented = present_json_popup(gobj, $box, {
2797
+ name: "filewin_" + clean_name(gobj_name(gobj)),
2798
+ logical_class: "TREEDB_CELL_FILE",
2799
+ title_prefix: String(row_id),
2800
+ title: col_id,
2801
+ icon: "yi-eye",
2802
+ forget_window: function() {
2803
+ priv.cell_file_win = null;
2804
+ },
2805
+ on_close: function() {
2806
+ teardown_cell_file(gobj);
2807
+ }
2808
+ });
2809
+ if(!presented) {
2810
+ return; // Error already logged
2811
+ }
2812
+ priv.cell_file_box = $box;
2813
+ priv.cell_file_modal = presented.modal;
2814
+ priv.cell_file_win = presented.win;
2815
+
2816
+ ids.forEach((id, i) => {
2817
+ request_asset(gobj, id, {target: "popup", box: $box, slot: i});
2818
+ });
2819
+ }
2820
+
2821
+ /************************************************************
2822
+ * Fill one slot of the file popup with what came back.
2823
+ ************************************************************/
2824
+ function fill_cell_file_slot(gobj, pend, answer, error)
2825
+ {
2826
+ let priv = gobj.priv;
2827
+ if(!priv.cell_file_box || priv.cell_file_box !== pend.box) {
2828
+ return; /* that popup was closed or replaced: nobody to show it to */
2829
+ }
2830
+ let $item = pend.box.querySelectorAll('.CELL_FILE_ITEM')[pend.slot];
2831
+ let $slot = $item? $item.querySelector('.CELL_FILE_SLOT') : null;
2832
+ let $caption = $item? $item.querySelector('.CELL_FILE_CAPTION') : null;
2833
+ if(!$slot || !$caption) {
2834
+ log_error(`${gobj_short_name(gobj)}: file popup has no slot ${pend.slot}`);
2835
+ return;
2836
+ }
2837
+
2838
+ yui_asset_release($slot);
2839
+ $slot.replaceChildren(yui_asset_element(error? null: answer, {
2840
+ detail: error || pend.id,
2841
+ alt: pend.id,
2842
+ class: 'CELL_FILE_MEDIA'
2843
+ }));
2844
+ refresh_language($slot, t);
2845
+
2846
+ if(!error && answer) {
2847
+ let meta = [answer.content_type, yui_file_size_label(answer.size)]
2848
+ .filter((s) => !!s).join(" · ");
2849
+ if(meta) {
2850
+ $caption.appendChild(
2851
+ createElement2(['span', {class: 'CELL_FILE_META'}, meta])
2852
+ );
2853
+ }
2854
+ /* Full size, in the browser's own viewer: a photo is read at its
2855
+ * size, not at the popup's. */
2856
+ let $open = yui_asset_open_link(answer);
2857
+ if($open) {
2858
+ $caption.appendChild($open);
2859
+ }
2860
+ }
2861
+ }
2862
+
2863
+ /************************************************************
2864
+ * Retire the file popup. Called from its on_close -- the
2865
+ * shell or the window has already removed the DOM. The blob
2866
+ * urls it made are revoked; answers still in flight for it
2867
+ * land on nobody (fill_cell_file_slot checks the box).
2868
+ ************************************************************/
2869
+ function teardown_cell_file(gobj)
2870
+ {
2871
+ let priv = gobj.priv;
2872
+ yui_asset_release(priv.cell_file_box);
2873
+ priv.cell_file_box = null;
2874
+ let win = priv.cell_file_win;
2875
+ priv.cell_file_win = null;
2876
+ retire_popup_window(win);
2877
+ priv.cell_file_modal = null;
2878
+ }
2879
+
2880
+ /************************************************************
2881
+ * Close the file popup (teardown, or a second open).
2882
+ ************************************************************/
2883
+ function close_cell_file_dialog(gobj)
2884
+ {
2885
+ let priv = gobj.priv;
2886
+ if(priv.cell_file_modal) {
2887
+ let modal = priv.cell_file_modal;
2888
+ priv.cell_file_modal = null;
2889
+ modal.close(); // -> on_close -> teardown_cell_file
2890
+ return;
2891
+ }
2892
+ teardown_cell_file(gobj);
2893
+ }
2894
+
2576
2895
  /************************************************************
2577
2896
  * True if the topic's pkey col carries the "rowid" flag.
2578
2897
  ************************************************************/
@@ -3836,6 +4155,72 @@ function ac_show_cell_json(gobj, event, kw, src)
3836
4155
  return 0;
3837
4156
  }
3838
4157
 
4158
+ /************************************************************
4159
+ * Show the asset(s) one `file` cell names
4160
+ * {
4161
+ * row_id:
4162
+ * col_id:
4163
+ * }
4164
+ ************************************************************/
4165
+ function ac_show_cell_file(gobj, event, kw, src)
4166
+ {
4167
+ open_cell_file_dialog(gobj, kw.row_id, kw.col_id);
4168
+
4169
+ return 0;
4170
+ }
4171
+
4172
+ /************************************************************
4173
+ * Hand an asset answer -- or why there is none -- to whoever
4174
+ * asked for it, if it is still there to take it.
4175
+ ************************************************************/
4176
+ function deliver_asset(gobj, pend, answer, error)
4177
+ {
4178
+ let priv = gobj.priv;
4179
+ if(pend.target === "form") {
4180
+ if(!priv.form || priv.form !== pend.form) {
4181
+ return; /* that form was closed: nobody to show it to */
4182
+ }
4183
+ gobj_send_event(priv.form, "EV_SET_FILE_PREVIEW", {
4184
+ name: pend.col,
4185
+ id: pend.id,
4186
+ answer: answer,
4187
+ error: error
4188
+ }, gobj);
4189
+ return;
4190
+ }
4191
+ fill_cell_file_slot(gobj, pend, answer, error);
4192
+ }
4193
+
4194
+ /************************************************************
4195
+ * The host answered an asset.
4196
+ * {req_id, answer}
4197
+ ************************************************************/
4198
+ function ac_asset_loaded(gobj, event, kw, src)
4199
+ {
4200
+ let pend = take_pending_asset(gobj, kw.req_id);
4201
+ if(!pend) {
4202
+ return 0; /* already settled: a late answer after a timeout */
4203
+ }
4204
+ deliver_asset(gobj, pend, kw.answer || null, "");
4205
+ return 0;
4206
+ }
4207
+
4208
+ /************************************************************
4209
+ * The host could not answer it, or nobody did in time.
4210
+ * {req_id, error}
4211
+ ************************************************************/
4212
+ function ac_asset_failed(gobj, event, kw, src)
4213
+ {
4214
+ let why = (kw && kw.error) || "asset request failed";
4215
+ let pend = take_pending_asset(gobj, kw.req_id);
4216
+ log_error(`${gobj_short_name(gobj)}: asset '${pend? pend.id : kw.req_id}': ${why}`);
4217
+ if(!pend) {
4218
+ return 0; /* already settled */
4219
+ }
4220
+ deliver_asset(gobj, pend, null, why);
4221
+ return 0;
4222
+ }
4223
+
3839
4224
  /************************************************************
3840
4225
  * One cell of one record changed. Publish it UP as a field write;
3841
4226
  * the host owns the treedb command and its options.
@@ -4336,6 +4721,9 @@ function create_gclass(gclass_name)
4336
4721
  ["EV_PASTE_ROWS", ac_paste_rows, null],
4337
4722
  ["EV_SHOW_HOOK_DATA", ac_show_hook_data, null],
4338
4723
  ["EV_SHOW_CELL_JSON", ac_show_cell_json, null],
4724
+ ["EV_SHOW_CELL_FILE", ac_show_cell_file, null],
4725
+ ["EV_ASSET_LOADED", ac_asset_loaded, null],
4726
+ ["EV_ASSET_FAILED", ac_asset_failed, null],
4339
4727
  ["EV_CHANGE_LOCALE", ac_change_locale, null],
4340
4728
  ["EV_REFRESH", ac_refresh, null],
4341
4729
  ["EV_SHOW_SCHEMA", ac_show_schema, null],
@@ -4378,6 +4766,10 @@ function create_gclass(gclass_name)
4378
4766
  ["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
4379
4767
  ["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
4380
4768
  ["EV_SHOW_CELL_JSON", 0],
4769
+ ["EV_SHOW_CELL_FILE", 0],
4770
+ ["EV_ASSET_LOADED", 0],
4771
+ ["EV_ASSET_FAILED", 0],
4772
+ ["EV_REQUEST_ASSET", event_flag_t.EVF_OUTPUT_EVENT],
4381
4773
  ["EV_CHANGE_LOCALE", 0],
4382
4774
  ["EV_REFRESH", 0],
4383
4775
  ["EV_SHOW_SCHEMA", 0],