@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/README.md +43 -1
- package/dist/gobj-ui.cjs.js +773 -125
- package/dist/gobj-ui.es.js +773 -125
- package/package.json +1 -1
- package/src/c_yui_form.js +52 -1
- package/src/c_yui_treedb_topic_with_form.css +67 -0
- package/src/c_yui_treedb_topic_with_form.js +401 -9
- package/src/c_yui_treedb_topics.js +80 -0
- package/src/yui_asset.css +51 -0
- package/src/yui_asset.js +223 -3
- package/src/yui_asset.test.js +18 -0
- package/src/yui_file_field.css +28 -0
- package/src/yui_file_field.js +45 -1
package/package.json
CHANGED
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 {
|
|
100
|
-
|
|
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
|
|
1839
|
-
* blank -- a blank cell and a cell whose asset failed
|
|
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
|
|
1845
|
-
if(!
|
|
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
|
-
[
|
|
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],
|