@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/dist/gobj-ui.es.js
CHANGED
|
@@ -6090,6 +6090,299 @@ function yui_tabulator_relocalize(table, t) {
|
|
|
6090
6090
|
}
|
|
6091
6091
|
}
|
|
6092
6092
|
//#endregion
|
|
6093
|
+
//#region src/yui_asset.js
|
|
6094
|
+
var BLOB_ATTR = "data-yui-blob";
|
|
6095
|
+
function ref_to_id(ref) {
|
|
6096
|
+
if (!ref) return null;
|
|
6097
|
+
if (typeof ref === "string") {
|
|
6098
|
+
if (ref.indexOf("^") < 0) return ref;
|
|
6099
|
+
const parts = ref.split("^");
|
|
6100
|
+
return parts.length >= 2 && parts[1] ? parts[1] : null;
|
|
6101
|
+
}
|
|
6102
|
+
if (typeof ref === "object" && typeof ref.id === "string") return ref.id;
|
|
6103
|
+
return null;
|
|
6104
|
+
}
|
|
6105
|
+
/***************************************************************************
|
|
6106
|
+
* Every asset id a column names, in order. Always an array, possibly empty.
|
|
6107
|
+
***************************************************************************/
|
|
6108
|
+
function yui_asset_ids(ref) {
|
|
6109
|
+
if (Array.isArray(ref)) return ref.map(ref_to_id).filter((id) => !!id);
|
|
6110
|
+
const id = ref_to_id(ref);
|
|
6111
|
+
return id ? [id] : [];
|
|
6112
|
+
}
|
|
6113
|
+
/***************************************************************************
|
|
6114
|
+
* The first asset id a column names, or null.
|
|
6115
|
+
***************************************************************************/
|
|
6116
|
+
function yui_asset_id(ref) {
|
|
6117
|
+
const ids = yui_asset_ids(ref);
|
|
6118
|
+
return ids.length ? ids[0] : null;
|
|
6119
|
+
}
|
|
6120
|
+
/***************************************************************************
|
|
6121
|
+
* Turn a `get-asset` answer into something an element can load.
|
|
6122
|
+
*
|
|
6123
|
+
* Returns null when the answer carries neither -- which the caller must
|
|
6124
|
+
* treat as "missing", never as an empty src: an <img src=""> reloads the
|
|
6125
|
+
* PAGE in some browsers.
|
|
6126
|
+
***************************************************************************/
|
|
6127
|
+
function yui_asset_src(answer) {
|
|
6128
|
+
if (!answer) return null;
|
|
6129
|
+
if (answer.mode === "url" && answer.url) return answer.url;
|
|
6130
|
+
if (answer.mode === "inline" && answer.content64) return `data:${answer.content_type || "application/octet-stream"};base64,${answer.content64}`;
|
|
6131
|
+
return null;
|
|
6132
|
+
}
|
|
6133
|
+
/***************************************************************************
|
|
6134
|
+
* The marker for an asset that is not there.
|
|
6135
|
+
*
|
|
6136
|
+
* A missing image used to leave a broken box and no word about it, which
|
|
6137
|
+
* is indistinguishable from a slow one and from a bug. This says so, and
|
|
6138
|
+
* it says WHICH: `detail` is data (a name, a path) and is never
|
|
6139
|
+
* translated, while the label carries its i18n key so a language change
|
|
6140
|
+
* re-translates it.
|
|
6141
|
+
***************************************************************************/
|
|
6142
|
+
function yui_asset_missing(detail, opts = {}) {
|
|
6143
|
+
const key = opts.key || "asset not available";
|
|
6144
|
+
const children = [["span", { class: "ASSET_MISSING_ICON yi-triangle-exclamation" }], [
|
|
6145
|
+
"span",
|
|
6146
|
+
{
|
|
6147
|
+
class: "ASSET_MISSING_LABEL",
|
|
6148
|
+
i18n: key
|
|
6149
|
+
},
|
|
6150
|
+
key
|
|
6151
|
+
]];
|
|
6152
|
+
if (detail) children.push([
|
|
6153
|
+
"span",
|
|
6154
|
+
{ class: "ASSET_MISSING_DETAIL" },
|
|
6155
|
+
String(detail)
|
|
6156
|
+
]);
|
|
6157
|
+
const attrs = { class: `ASSET_MISSING ${opts.class || ""}`.trim() };
|
|
6158
|
+
if (detail) attrs.title = String(detail);
|
|
6159
|
+
else attrs["data-i18n-title"] = key;
|
|
6160
|
+
return createElement2([
|
|
6161
|
+
"div",
|
|
6162
|
+
attrs,
|
|
6163
|
+
children
|
|
6164
|
+
]);
|
|
6165
|
+
}
|
|
6166
|
+
/***************************************************************************
|
|
6167
|
+
* What KIND of thing a content type is, as far as showing it goes:
|
|
6168
|
+
*
|
|
6169
|
+
* "image" | "video" | "audio" the browser draws it in an element
|
|
6170
|
+
* "pdf" the browser's own viewer, in a frame
|
|
6171
|
+
* "other" nothing here can draw it: a card
|
|
6172
|
+
* saying what it is, and a way to open
|
|
6173
|
+
*
|
|
6174
|
+
* The content type is the one the BACKEND stored (treedb checks it
|
|
6175
|
+
* against the bytes), never the extension of a name a person typed.
|
|
6176
|
+
***************************************************************************/
|
|
6177
|
+
function yui_asset_kind(content_type) {
|
|
6178
|
+
const ct = String(content_type || "").toLowerCase().split(";")[0].trim();
|
|
6179
|
+
if (ct.startsWith("image/")) return "image";
|
|
6180
|
+
if (ct.startsWith("video/")) return "video";
|
|
6181
|
+
if (ct.startsWith("audio/")) return "audio";
|
|
6182
|
+
if (ct === "application/pdf") return "pdf";
|
|
6183
|
+
return "other";
|
|
6184
|
+
}
|
|
6185
|
+
/***************************************************************************
|
|
6186
|
+
* A `blob:` url for inline bytes, or null.
|
|
6187
|
+
*
|
|
6188
|
+
* A frame and a new tab need a blob and not a data url: Firefox refuses to
|
|
6189
|
+
* NAVIGATE to a data url (a new tab opened on one stays blank), and its
|
|
6190
|
+
* PDF viewer does not run on one inside a frame.
|
|
6191
|
+
***************************************************************************/
|
|
6192
|
+
function base64_to_blob_url(content64, content_type) {
|
|
6193
|
+
try {
|
|
6194
|
+
const binary = atob(content64);
|
|
6195
|
+
const bytes = new Uint8Array(binary.length);
|
|
6196
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
6197
|
+
const blob = new Blob([bytes], { type: content_type || "application/octet-stream" });
|
|
6198
|
+
return URL.createObjectURL(blob);
|
|
6199
|
+
} catch (e) {
|
|
6200
|
+
log_error(`cannot decode the bytes of an asset: ${e && e.message ? e.message : e}`);
|
|
6201
|
+
return null;
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
/***************************************************************************
|
|
6205
|
+
* Where a frame or a link can take an asset:
|
|
6206
|
+
*
|
|
6207
|
+
* -> {href, blob} `blob` true when href is a blob url THIS call
|
|
6208
|
+
* created, which the caller must mark (BLOB_ATTR)
|
|
6209
|
+
* so that yui_asset_release() revokes it
|
|
6210
|
+
* -> null nothing to open
|
|
6211
|
+
***************************************************************************/
|
|
6212
|
+
function yui_asset_href(answer) {
|
|
6213
|
+
if (!answer) return null;
|
|
6214
|
+
if (answer.mode === "url" && answer.url) return {
|
|
6215
|
+
href: answer.url,
|
|
6216
|
+
blob: false
|
|
6217
|
+
};
|
|
6218
|
+
if (answer.mode === "inline" && answer.content64) {
|
|
6219
|
+
const href = base64_to_blob_url(answer.content64, answer.content_type);
|
|
6220
|
+
return href ? {
|
|
6221
|
+
href,
|
|
6222
|
+
blob: true
|
|
6223
|
+
} : null;
|
|
6224
|
+
}
|
|
6225
|
+
return null;
|
|
6226
|
+
}
|
|
6227
|
+
/***************************************************************************
|
|
6228
|
+
* The answer a picked `File` would have had from the backend.
|
|
6229
|
+
*
|
|
6230
|
+
* A file a person picked and has not saved yet is shown BEFORE it exists
|
|
6231
|
+
* as an asset, so it goes through the same element as a stored one: a
|
|
6232
|
+
* `url` answer whose url is a blob url over the File itself -- which
|
|
6233
|
+
* reads nothing until the browser draws it.
|
|
6234
|
+
*
|
|
6235
|
+
* `blob: true` says the url is owned: whoever drops the element revokes
|
|
6236
|
+
* it with yui_asset_release().
|
|
6237
|
+
***************************************************************************/
|
|
6238
|
+
function yui_asset_file_answer(file) {
|
|
6239
|
+
if (!file) return null;
|
|
6240
|
+
let url;
|
|
6241
|
+
try {
|
|
6242
|
+
url = URL.createObjectURL(file);
|
|
6243
|
+
} catch (e) {
|
|
6244
|
+
log_error(`cannot preview the picked file: ${e && e.message ? e.message : e}`);
|
|
6245
|
+
return null;
|
|
6246
|
+
}
|
|
6247
|
+
return {
|
|
6248
|
+
mode: "url",
|
|
6249
|
+
url,
|
|
6250
|
+
content_type: file.type || "",
|
|
6251
|
+
size: file.size,
|
|
6252
|
+
original_name: file.name || "",
|
|
6253
|
+
blob: true
|
|
6254
|
+
};
|
|
6255
|
+
}
|
|
6256
|
+
/***************************************************************************
|
|
6257
|
+
* Revoke every blob url this module created under `$root`, $root included.
|
|
6258
|
+
*
|
|
6259
|
+
* Call it on whatever element is about to be dropped: a preview being
|
|
6260
|
+
* replaced, a dialog being closed. A blob url that is never revoked keeps
|
|
6261
|
+
* its bytes in memory for the life of the page.
|
|
6262
|
+
***************************************************************************/
|
|
6263
|
+
function yui_asset_release($root) {
|
|
6264
|
+
if (!$root || !$root.querySelectorAll) return;
|
|
6265
|
+
const marked = Array.from($root.querySelectorAll(`[${BLOB_ATTR}]`));
|
|
6266
|
+
if ($root.hasAttribute && $root.hasAttribute(BLOB_ATTR)) marked.push($root);
|
|
6267
|
+
marked.forEach(($el) => {
|
|
6268
|
+
const url = $el.getAttribute(BLOB_ATTR);
|
|
6269
|
+
$el.removeAttribute(BLOB_ATTR);
|
|
6270
|
+
if (url) URL.revokeObjectURL(url);
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
/***************************************************************************
|
|
6274
|
+
* The link that opens an asset in a new tab, or null when there is
|
|
6275
|
+
* nothing to open. A control: named for the eye and for a reader, both
|
|
6276
|
+
* through their keys so a language change re-translates them.
|
|
6277
|
+
***************************************************************************/
|
|
6278
|
+
function yui_asset_open_link(answer, opts = {}) {
|
|
6279
|
+
const target = yui_asset_href(answer);
|
|
6280
|
+
if (!target) return null;
|
|
6281
|
+
const key = "open in a new tab";
|
|
6282
|
+
const $a = createElement2([
|
|
6283
|
+
"a",
|
|
6284
|
+
{
|
|
6285
|
+
class: `ASSET_OPEN ${opts.class || ""}`.trim(),
|
|
6286
|
+
target: "_blank",
|
|
6287
|
+
rel: "noopener",
|
|
6288
|
+
title: t(key),
|
|
6289
|
+
"data-i18n-title": key,
|
|
6290
|
+
"aria-label": t(key),
|
|
6291
|
+
"data-i18n-aria-label": key
|
|
6292
|
+
},
|
|
6293
|
+
[["span", { class: "ASSET_OPEN_ICON yi-link" }], [
|
|
6294
|
+
"span",
|
|
6295
|
+
{
|
|
6296
|
+
class: "ASSET_OPEN_LABEL",
|
|
6297
|
+
i18n: key
|
|
6298
|
+
},
|
|
6299
|
+
key
|
|
6300
|
+
]]
|
|
6301
|
+
]);
|
|
6302
|
+
$a.href = target.href;
|
|
6303
|
+
if (target.blob) $a.setAttribute(BLOB_ATTR, target.href);
|
|
6304
|
+
return $a;
|
|
6305
|
+
}
|
|
6306
|
+
/***************************************************************************
|
|
6307
|
+
* The element for one asset.
|
|
6308
|
+
*
|
|
6309
|
+
* The KIND comes from the content type the backend stored, not from the
|
|
6310
|
+
* name a person typed: video and audio are assets too, and an <img> whose
|
|
6311
|
+
* src is a film shows the broken box this module exists to remove.
|
|
6312
|
+
*
|
|
6313
|
+
* `detail` is what the missing marker shows -- pass the original name or
|
|
6314
|
+
* the source path, the thing a person can act on.
|
|
6315
|
+
*
|
|
6316
|
+
* A PDF goes in a frame, where the browser's own viewer draws it. Any
|
|
6317
|
+
* other kind gets a card: what it is, and a link to open it.
|
|
6318
|
+
* TODO: draw more kinds in place (plain text, office documents); today
|
|
6319
|
+
* they are only opened in a new tab, where the browser does what it can.
|
|
6320
|
+
***************************************************************************/
|
|
6321
|
+
function yui_asset_element(answer, opts = {}) {
|
|
6322
|
+
const src = yui_asset_src(answer);
|
|
6323
|
+
if (!src) return yui_asset_missing(opts.detail, opts);
|
|
6324
|
+
const kind = yui_asset_kind(answer.content_type);
|
|
6325
|
+
const cls = `ASSET_MEDIA ${opts.class || ""}`.trim();
|
|
6326
|
+
if (kind === "pdf") {
|
|
6327
|
+
const target = yui_asset_href(answer);
|
|
6328
|
+
if (!target) return yui_asset_missing(opts.detail, opts);
|
|
6329
|
+
const $frame = createElement2(["iframe", {
|
|
6330
|
+
class: `${cls} ASSET_DOCUMENT`,
|
|
6331
|
+
title: opts.detail || answer.content_type || ""
|
|
6332
|
+
}]);
|
|
6333
|
+
$frame.src = target.href;
|
|
6334
|
+
if (target.blob) $frame.setAttribute(BLOB_ATTR, target.href);
|
|
6335
|
+
return $frame;
|
|
6336
|
+
}
|
|
6337
|
+
if (kind === "other") {
|
|
6338
|
+
const children = [[
|
|
6339
|
+
"span",
|
|
6340
|
+
{ class: "ASSET_OTHER_TYPE" },
|
|
6341
|
+
String(answer.content_type || "?")
|
|
6342
|
+
]];
|
|
6343
|
+
if (opts.detail) children.push([
|
|
6344
|
+
"span",
|
|
6345
|
+
{ class: "ASSET_OTHER_DETAIL" },
|
|
6346
|
+
String(opts.detail)
|
|
6347
|
+
]);
|
|
6348
|
+
const $card = createElement2([
|
|
6349
|
+
"div",
|
|
6350
|
+
{ class: `${cls} ASSET_OTHER` },
|
|
6351
|
+
children
|
|
6352
|
+
]);
|
|
6353
|
+
const $open = yui_asset_open_link(answer);
|
|
6354
|
+
if ($open) $card.appendChild($open);
|
|
6355
|
+
return $card;
|
|
6356
|
+
}
|
|
6357
|
+
let el;
|
|
6358
|
+
if (kind === "video") el = createElement2(["video", {
|
|
6359
|
+
class: cls,
|
|
6360
|
+
controls: "",
|
|
6361
|
+
preload: "metadata"
|
|
6362
|
+
}]);
|
|
6363
|
+
else if (kind === "audio") el = createElement2(["audio", {
|
|
6364
|
+
class: cls,
|
|
6365
|
+
controls: "",
|
|
6366
|
+
preload: "metadata"
|
|
6367
|
+
}]);
|
|
6368
|
+
else {
|
|
6369
|
+
el = createElement2(["img", {
|
|
6370
|
+
class: cls,
|
|
6371
|
+
loading: "lazy"
|
|
6372
|
+
}]);
|
|
6373
|
+
el.alt = opts.alt || opts.detail || "";
|
|
6374
|
+
}
|
|
6375
|
+
el.src = src;
|
|
6376
|
+
if (answer.blob) el.setAttribute(BLOB_ATTR, src);
|
|
6377
|
+
el.onerror = () => {
|
|
6378
|
+
log_error(`asset could not be loaded: ${opts.detail || src}`);
|
|
6379
|
+
const $marker = yui_asset_missing(opts.detail, opts);
|
|
6380
|
+
if (el.parentNode) el.parentNode.replaceChild($marker, el);
|
|
6381
|
+
yui_asset_release(el);
|
|
6382
|
+
};
|
|
6383
|
+
return el;
|
|
6384
|
+
}
|
|
6385
|
+
//#endregion
|
|
6093
6386
|
//#region src/yui_file_field.js
|
|
6094
6387
|
/***********************************************************************
|
|
6095
6388
|
* yui_file_field.js
|
|
@@ -6326,6 +6619,10 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
|
|
|
6326
6619
|
const $name = createElement2(["span", { class: "FILE_NAME" }]);
|
|
6327
6620
|
const $size = createElement2(["span", { class: "FILE_SIZE" }]);
|
|
6328
6621
|
const $state = createElement2(["span", { class: "FILE_STATE" }]);
|
|
6622
|
+
const $preview = createElement2(["div", {
|
|
6623
|
+
class: "FILE_PREVIEW",
|
|
6624
|
+
hidden: ""
|
|
6625
|
+
}]);
|
|
6329
6626
|
const $control = createElement2([
|
|
6330
6627
|
"div",
|
|
6331
6628
|
{ class: "FILE_FIELD" },
|
|
@@ -6335,11 +6632,13 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
|
|
|
6335
6632
|
$name,
|
|
6336
6633
|
$size,
|
|
6337
6634
|
$state,
|
|
6338
|
-
$clear
|
|
6635
|
+
$clear,
|
|
6636
|
+
$preview
|
|
6339
6637
|
]
|
|
6340
6638
|
]);
|
|
6341
6639
|
$control.yui_file = null;
|
|
6342
6640
|
$control.yui_file_value = value || "";
|
|
6641
|
+
$control.yui_file_stored = null;
|
|
6343
6642
|
const render = () => {
|
|
6344
6643
|
const picked = $control.yui_file;
|
|
6345
6644
|
const kept = $control.yui_file_value;
|
|
@@ -6360,6 +6659,24 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
|
|
|
6360
6659
|
}
|
|
6361
6660
|
$clear.hidden = readonly || !picked && !kept;
|
|
6362
6661
|
$pick.hidden = !!readonly;
|
|
6662
|
+
yui_asset_release($preview);
|
|
6663
|
+
$preview.replaceChildren();
|
|
6664
|
+
let $media = null;
|
|
6665
|
+
if (picked) {
|
|
6666
|
+
const answer = yui_asset_file_answer(picked);
|
|
6667
|
+
if (answer) $media = yui_asset_element(answer, {
|
|
6668
|
+
detail: picked.name || "",
|
|
6669
|
+
class: "FILE_PREVIEW_MEDIA"
|
|
6670
|
+
});
|
|
6671
|
+
} else if (kept) {
|
|
6672
|
+
const stored = $control.yui_file_stored;
|
|
6673
|
+
if (stored && stored.id === kept) $media = yui_asset_element(stored.answer, {
|
|
6674
|
+
detail: stored.error || kept,
|
|
6675
|
+
class: "FILE_PREVIEW_MEDIA"
|
|
6676
|
+
});
|
|
6677
|
+
}
|
|
6678
|
+
if ($media) $preview.appendChild($media);
|
|
6679
|
+
$preview.hidden = !$media;
|
|
6363
6680
|
};
|
|
6364
6681
|
if (!readonly) {
|
|
6365
6682
|
$pick.addEventListener("click", () => {
|
|
@@ -6388,120 +6705,6 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
|
|
|
6388
6705
|
};
|
|
6389
6706
|
}
|
|
6390
6707
|
//#endregion
|
|
6391
|
-
//#region src/yui_asset.js
|
|
6392
|
-
function ref_to_id(ref) {
|
|
6393
|
-
if (!ref) return null;
|
|
6394
|
-
if (typeof ref === "string") {
|
|
6395
|
-
if (ref.indexOf("^") < 0) return ref;
|
|
6396
|
-
const parts = ref.split("^");
|
|
6397
|
-
return parts.length >= 2 && parts[1] ? parts[1] : null;
|
|
6398
|
-
}
|
|
6399
|
-
if (typeof ref === "object" && typeof ref.id === "string") return ref.id;
|
|
6400
|
-
return null;
|
|
6401
|
-
}
|
|
6402
|
-
/***************************************************************************
|
|
6403
|
-
* Every asset id a column names, in order. Always an array, possibly empty.
|
|
6404
|
-
***************************************************************************/
|
|
6405
|
-
function yui_asset_ids(ref) {
|
|
6406
|
-
if (Array.isArray(ref)) return ref.map(ref_to_id).filter((id) => !!id);
|
|
6407
|
-
const id = ref_to_id(ref);
|
|
6408
|
-
return id ? [id] : [];
|
|
6409
|
-
}
|
|
6410
|
-
/***************************************************************************
|
|
6411
|
-
* The first asset id a column names, or null.
|
|
6412
|
-
***************************************************************************/
|
|
6413
|
-
function yui_asset_id(ref) {
|
|
6414
|
-
const ids = yui_asset_ids(ref);
|
|
6415
|
-
return ids.length ? ids[0] : null;
|
|
6416
|
-
}
|
|
6417
|
-
/***************************************************************************
|
|
6418
|
-
* Turn a `get-asset` answer into something an element can load.
|
|
6419
|
-
*
|
|
6420
|
-
* Returns null when the answer carries neither -- which the caller must
|
|
6421
|
-
* treat as "missing", never as an empty src: an <img src=""> reloads the
|
|
6422
|
-
* PAGE in some browsers.
|
|
6423
|
-
***************************************************************************/
|
|
6424
|
-
function yui_asset_src(answer) {
|
|
6425
|
-
if (!answer) return null;
|
|
6426
|
-
if (answer.mode === "url" && answer.url) return answer.url;
|
|
6427
|
-
if (answer.mode === "inline" && answer.content64) return `data:${answer.content_type || "application/octet-stream"};base64,${answer.content64}`;
|
|
6428
|
-
return null;
|
|
6429
|
-
}
|
|
6430
|
-
/***************************************************************************
|
|
6431
|
-
* The marker for an asset that is not there.
|
|
6432
|
-
*
|
|
6433
|
-
* A missing image used to leave a broken box and no word about it, which
|
|
6434
|
-
* is indistinguishable from a slow one and from a bug. This says so, and
|
|
6435
|
-
* it says WHICH: `detail` is data (a name, a path) and is never
|
|
6436
|
-
* translated, while the label carries its i18n key so a language change
|
|
6437
|
-
* re-translates it.
|
|
6438
|
-
***************************************************************************/
|
|
6439
|
-
function yui_asset_missing(detail, opts = {}) {
|
|
6440
|
-
const key = opts.key || "asset not available";
|
|
6441
|
-
const children = [["span", { class: "ASSET_MISSING_ICON yi-triangle-exclamation" }], [
|
|
6442
|
-
"span",
|
|
6443
|
-
{
|
|
6444
|
-
class: "ASSET_MISSING_LABEL",
|
|
6445
|
-
i18n: key
|
|
6446
|
-
},
|
|
6447
|
-
key
|
|
6448
|
-
]];
|
|
6449
|
-
if (detail) children.push([
|
|
6450
|
-
"span",
|
|
6451
|
-
{ class: "ASSET_MISSING_DETAIL" },
|
|
6452
|
-
String(detail)
|
|
6453
|
-
]);
|
|
6454
|
-
const attrs = { class: `ASSET_MISSING ${opts.class || ""}`.trim() };
|
|
6455
|
-
if (detail) attrs.title = String(detail);
|
|
6456
|
-
else attrs["data-i18n-title"] = key;
|
|
6457
|
-
return createElement2([
|
|
6458
|
-
"div",
|
|
6459
|
-
attrs,
|
|
6460
|
-
children
|
|
6461
|
-
]);
|
|
6462
|
-
}
|
|
6463
|
-
/***************************************************************************
|
|
6464
|
-
* The element for one asset.
|
|
6465
|
-
*
|
|
6466
|
-
* The KIND comes from the content type the backend stored, not from the
|
|
6467
|
-
* name a person typed: video and audio are assets too, and an <img> whose
|
|
6468
|
-
* src is a film shows the broken box this module exists to remove.
|
|
6469
|
-
*
|
|
6470
|
-
* `detail` is what the missing marker shows -- pass the original name or
|
|
6471
|
-
* the source path, the thing a person can act on.
|
|
6472
|
-
***************************************************************************/
|
|
6473
|
-
function yui_asset_element(answer, opts = {}) {
|
|
6474
|
-
const src = yui_asset_src(answer);
|
|
6475
|
-
if (!src) return yui_asset_missing(opts.detail, opts);
|
|
6476
|
-
const ct = (answer.content_type || "").toLowerCase();
|
|
6477
|
-
const cls = `ASSET_MEDIA ${opts.class || ""}`.trim();
|
|
6478
|
-
let el;
|
|
6479
|
-
if (ct.startsWith("video/")) el = createElement2(["video", {
|
|
6480
|
-
class: cls,
|
|
6481
|
-
controls: "",
|
|
6482
|
-
preload: "metadata"
|
|
6483
|
-
}]);
|
|
6484
|
-
else if (ct.startsWith("audio/")) el = createElement2(["audio", {
|
|
6485
|
-
class: cls,
|
|
6486
|
-
controls: "",
|
|
6487
|
-
preload: "metadata"
|
|
6488
|
-
}]);
|
|
6489
|
-
else {
|
|
6490
|
-
el = createElement2(["img", {
|
|
6491
|
-
class: cls,
|
|
6492
|
-
loading: "lazy"
|
|
6493
|
-
}]);
|
|
6494
|
-
el.alt = opts.alt || opts.detail || "";
|
|
6495
|
-
}
|
|
6496
|
-
el.src = src;
|
|
6497
|
-
el.onerror = () => {
|
|
6498
|
-
log_error(`asset could not be loaded: ${opts.detail || src}`);
|
|
6499
|
-
const $marker = yui_asset_missing(opts.detail, opts);
|
|
6500
|
-
if (el.parentNode) el.parentNode.replaceChild($marker, el);
|
|
6501
|
-
};
|
|
6502
|
-
return el;
|
|
6503
|
-
}
|
|
6504
|
-
//#endregion
|
|
6505
6708
|
//#region src/yui_inputs.js
|
|
6506
6709
|
/***************************************************************
|
|
6507
6710
|
* The ✕ shows only when there is something to clear AND the field
|
|
@@ -7171,6 +7374,7 @@ function mt_create$17(gobj) {
|
|
|
7171
7374
|
* and subscriptions are already deleted.
|
|
7172
7375
|
************************************************************/
|
|
7173
7376
|
function mt_destroy$17(gobj) {
|
|
7377
|
+
yui_asset_release(gobj_read_attr(gobj, "$container"));
|
|
7174
7378
|
destroy_ui$10(gobj);
|
|
7175
7379
|
}
|
|
7176
7380
|
/************************************************************
|
|
@@ -9319,6 +9523,43 @@ function ac_clear_record(gobj, event, kw, src) {
|
|
|
9319
9523
|
return 0;
|
|
9320
9524
|
}
|
|
9321
9525
|
/************************************************************
|
|
9526
|
+
* From the host: what it fetched for the asset a `file` column
|
|
9527
|
+
* names -- the `get-asset` answer, or why there is none.
|
|
9528
|
+
* {
|
|
9529
|
+
* name: the column
|
|
9530
|
+
* id: the asset id the answer is for
|
|
9531
|
+
* answer: the get-asset answer, or null
|
|
9532
|
+
* error: why there is no answer, when there is none
|
|
9533
|
+
* }
|
|
9534
|
+
*
|
|
9535
|
+
* Kept on the control and drawn only while the column still names
|
|
9536
|
+
* THAT id: an answer landing after the person picked another file
|
|
9537
|
+
* or cleared the column must not bring the old picture back.
|
|
9538
|
+
************************************************************/
|
|
9539
|
+
function ac_set_file_preview(gobj, event, kw, src) {
|
|
9540
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
9541
|
+
let $form = $container ? $container.querySelector("form") : null;
|
|
9542
|
+
if (!$form) {
|
|
9543
|
+
log_error(`${gobj_short_name(gobj)}: no form to show the file preview in`);
|
|
9544
|
+
return -1;
|
|
9545
|
+
}
|
|
9546
|
+
let $file = null;
|
|
9547
|
+
$form.querySelectorAll(".FILE_FIELD").forEach(($el) => {
|
|
9548
|
+
if ($el.dataset.treedb_name === kw.name) $file = $el;
|
|
9549
|
+
});
|
|
9550
|
+
if (!$file) {
|
|
9551
|
+
log_error(`${gobj_short_name(gobj)}: no file column '${kw.name}' in the form`);
|
|
9552
|
+
return -1;
|
|
9553
|
+
}
|
|
9554
|
+
$file.yui_file_stored = {
|
|
9555
|
+
id: kw.id || "",
|
|
9556
|
+
answer: kw.answer || null,
|
|
9557
|
+
error: kw.error || ""
|
|
9558
|
+
};
|
|
9559
|
+
if ($file.yui_file_render) $file.yui_file_render();
|
|
9560
|
+
return 0;
|
|
9561
|
+
}
|
|
9562
|
+
/************************************************************
|
|
9322
9563
|
* From internal form button for new rows
|
|
9323
9564
|
* or from load_tabulator_data() to load data from backend
|
|
9324
9565
|
************************************************************/
|
|
@@ -9512,6 +9753,11 @@ function create_gclass$17(gclass_name) {
|
|
|
9512
9753
|
ac_paste_record,
|
|
9513
9754
|
null
|
|
9514
9755
|
],
|
|
9756
|
+
[
|
|
9757
|
+
"EV_SET_FILE_PREVIEW",
|
|
9758
|
+
ac_set_file_preview,
|
|
9759
|
+
null
|
|
9760
|
+
],
|
|
9515
9761
|
[
|
|
9516
9762
|
"EV_WINDOW_MOVED",
|
|
9517
9763
|
null,
|
|
@@ -9557,6 +9803,7 @@ function create_gclass$17(gclass_name) {
|
|
|
9557
9803
|
["EV_RECORD_CHANGED", 0],
|
|
9558
9804
|
["EV_COPY_RECORD", 0],
|
|
9559
9805
|
["EV_PASTE_RECORD", 0],
|
|
9806
|
+
["EV_SET_FILE_PREVIEW", 0],
|
|
9560
9807
|
["EV_WINDOW_MOVED", 0],
|
|
9561
9808
|
["EV_WINDOW_RESIZED", 0],
|
|
9562
9809
|
["EV_WINDOW_TO_CLOSE", 0],
|
|
@@ -22754,6 +23001,7 @@ var attrs_table$6 = [
|
|
|
22754
23001
|
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
22755
23002
|
SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
|
|
22756
23003
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
|
|
23004
|
+
SDATA(data_type_t.DTP_STRING, "assets_service", 0, "assets", "Remote service that answers `get-asset` (a C_ASSETS) for the bytes of the treedb's `file` columns: the form's preview and the table's file popup ask it. Empty: nothing is asked, and a file cell opens a marker saying the asset is not available"),
|
|
22757
23005
|
SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
|
|
22758
23006
|
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"),
|
|
22759
23007
|
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"),
|
|
@@ -24073,6 +24321,24 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
24073
24321
|
else gobj_send_event(jv, "EV_SET_JSON", { json: data }, gobj);
|
|
24074
24322
|
return 0;
|
|
24075
24323
|
}
|
|
24324
|
+
if (command === "get-asset") {
|
|
24325
|
+
let asset_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
24326
|
+
let req_id = kw_get_str(gobj, kw_command, "req_id", "", 0);
|
|
24327
|
+
let gobj_topic_form = gobj_find_child(gobj, { __gobj_name__: `${gobj_name(gobj)}?${asset_topic}` });
|
|
24328
|
+
if (!gobj_topic_form || !req_id) {
|
|
24329
|
+
log_error(`${gobj_short_name(gobj)}: get-asset answer for nobody: topic '${asset_topic}', req_id '${req_id}'`);
|
|
24330
|
+
return 0;
|
|
24331
|
+
}
|
|
24332
|
+
if (result < 0) gobj_send_event(gobj_topic_form, "EV_ASSET_FAILED", {
|
|
24333
|
+
req_id,
|
|
24334
|
+
error: comment || "get-asset failed"
|
|
24335
|
+
}, gobj);
|
|
24336
|
+
else gobj_send_event(gobj_topic_form, "EV_ASSET_LOADED", {
|
|
24337
|
+
req_id,
|
|
24338
|
+
answer: data
|
|
24339
|
+
}, gobj);
|
|
24340
|
+
return 0;
|
|
24341
|
+
}
|
|
24076
24342
|
if (result < 0) {
|
|
24077
24343
|
if (command === "descs") show_load_error$1(gobj, t(comment));
|
|
24078
24344
|
else {
|
|
@@ -24354,6 +24620,48 @@ function ac_request_page(gobj, event, kw, src) {
|
|
|
24354
24620
|
return 0;
|
|
24355
24621
|
}
|
|
24356
24622
|
/********************************************
|
|
24623
|
+
* Event from formtable: the bytes of one asset a `file` column names,
|
|
24624
|
+
* for the form's preview or the table's file popup.
|
|
24625
|
+
*
|
|
24626
|
+
* Asked to the ASSETS service and not to the treedb: storing is the
|
|
24627
|
+
* treedb's, the way out is C_ASSETS's (a signed url when a web server
|
|
24628
|
+
* sits in front of the store, the bytes inline when there is none). The
|
|
24629
|
+
* answer goes back down to the table that asked, by `req_id`.
|
|
24630
|
+
*
|
|
24631
|
+
* A read: a read-only treedb still answers it.
|
|
24632
|
+
********************************************/
|
|
24633
|
+
function ac_request_asset(gobj, event, kw, src) {
|
|
24634
|
+
if (!kw.req_id || !kw.asset_id) {
|
|
24635
|
+
log_error(`${gobj_short_name(gobj)}: an asset request with no id`);
|
|
24636
|
+
return -1;
|
|
24637
|
+
}
|
|
24638
|
+
let assets_service = gobj_read_str_attr(gobj, "assets_service");
|
|
24639
|
+
let gobj_remote_yuno = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
24640
|
+
if (!assets_service || !gobj_remote_yuno) {
|
|
24641
|
+
gobj_send_event(src, "EV_ASSET_FAILED", {
|
|
24642
|
+
req_id: kw.req_id,
|
|
24643
|
+
error: assets_service ? "cannot reach the backend" : "no assets service"
|
|
24644
|
+
}, gobj);
|
|
24645
|
+
return 0;
|
|
24646
|
+
}
|
|
24647
|
+
let ret = gobj_command(gobj_remote_yuno, "get-asset", {
|
|
24648
|
+
service: assets_service,
|
|
24649
|
+
asset_id: kw.asset_id,
|
|
24650
|
+
__md_command__: {
|
|
24651
|
+
topic_name: kw.topic_name || gobj_read_attr(src, "topic_name"),
|
|
24652
|
+
req_id: kw.req_id
|
|
24653
|
+
}
|
|
24654
|
+
}, gobj);
|
|
24655
|
+
if (ret) {
|
|
24656
|
+
log_error(ret);
|
|
24657
|
+
gobj_send_event(src, "EV_ASSET_FAILED", {
|
|
24658
|
+
req_id: kw.req_id,
|
|
24659
|
+
error: "cannot reach the backend"
|
|
24660
|
+
}, gobj);
|
|
24661
|
+
}
|
|
24662
|
+
return 0;
|
|
24663
|
+
}
|
|
24664
|
+
/********************************************
|
|
24357
24665
|
* Event from formtable: ONE field of one record, edited in place.
|
|
24358
24666
|
*
|
|
24359
24667
|
* Written as a PARTIAL update and, deliberately, with NO `autolink`.
|
|
@@ -24539,6 +24847,11 @@ function create_gclass$6(gclass_name) {
|
|
|
24539
24847
|
ac_request_page,
|
|
24540
24848
|
null
|
|
24541
24849
|
],
|
|
24850
|
+
[
|
|
24851
|
+
"EV_REQUEST_ASSET",
|
|
24852
|
+
ac_request_asset,
|
|
24853
|
+
null
|
|
24854
|
+
],
|
|
24542
24855
|
[
|
|
24543
24856
|
"EV_DELETE_RECORD",
|
|
24544
24857
|
ac_delete_record,
|
|
@@ -24619,6 +24932,7 @@ function create_gclass$6(gclass_name) {
|
|
|
24619
24932
|
["EV_UPDATE_RECORD", 0],
|
|
24620
24933
|
["EV_UPDATE_FIELD", 0],
|
|
24621
24934
|
["EV_REQUEST_PAGE", 0],
|
|
24935
|
+
["EV_REQUEST_ASSET", 0],
|
|
24622
24936
|
["EV_DELETE_RECORD", 0],
|
|
24623
24937
|
["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
24624
24938
|
["EV_REFRESH_TOPIC", 0],
|
|
@@ -25424,7 +25738,12 @@ var PRIVATE_DATA$5 = {
|
|
|
25424
25738
|
schema_win: null,
|
|
25425
25739
|
cell_json_gobj: null,
|
|
25426
25740
|
cell_json_modal: null,
|
|
25427
|
-
cell_json_win: null
|
|
25741
|
+
cell_json_win: null,
|
|
25742
|
+
cell_file_box: null,
|
|
25743
|
+
cell_file_modal: null,
|
|
25744
|
+
cell_file_win: null,
|
|
25745
|
+
_pending_assets: null,
|
|
25746
|
+
_asset_seq: 0
|
|
25428
25747
|
};
|
|
25429
25748
|
var __gclass__$5 = null;
|
|
25430
25749
|
function is_mobile$1() {
|
|
@@ -25557,6 +25876,8 @@ function mt_stop$5(gobj) {
|
|
|
25557
25876
|
close_form_dialog(gobj);
|
|
25558
25877
|
close_schema_dialog(gobj);
|
|
25559
25878
|
close_cell_json_dialog(gobj);
|
|
25879
|
+
close_cell_file_dialog(gobj);
|
|
25880
|
+
drop_pending_assets(gobj);
|
|
25560
25881
|
table__destroy(gobj);
|
|
25561
25882
|
}
|
|
25562
25883
|
/***************************************************************
|
|
@@ -25566,6 +25887,8 @@ function mt_destroy$5(gobj) {
|
|
|
25566
25887
|
close_form_dialog(gobj);
|
|
25567
25888
|
close_schema_dialog(gobj);
|
|
25568
25889
|
close_cell_json_dialog(gobj);
|
|
25890
|
+
close_cell_file_dialog(gobj);
|
|
25891
|
+
drop_pending_assets(gobj);
|
|
25569
25892
|
destroy_ui$2(gobj);
|
|
25570
25893
|
}
|
|
25571
25894
|
/************************************************************
|
|
@@ -26210,6 +26533,20 @@ function create_tabulator(gobj) {
|
|
|
26210
26533
|
break;
|
|
26211
26534
|
case "file":
|
|
26212
26535
|
colFormatter = file_cell_formatter;
|
|
26536
|
+
cellClick = function(e, cell) {
|
|
26537
|
+
if (!e.target.closest(".FILE_CELL_LINK")) return;
|
|
26538
|
+
e.stopPropagation();
|
|
26539
|
+
let pkey = desc.pkey || "id";
|
|
26540
|
+
let row_id = cell.getData()[pkey];
|
|
26541
|
+
if (row_id === void 0 || row_id === null) {
|
|
26542
|
+
log_error(`${gobj_short_name(gobj)}: row without pkey '${pkey}', cannot open the file of '${cell.getField()}'`);
|
|
26543
|
+
return;
|
|
26544
|
+
}
|
|
26545
|
+
gobj_send_event(gobj, "EV_SHOW_CELL_FILE", {
|
|
26546
|
+
row_id,
|
|
26547
|
+
col_id: cell.getField()
|
|
26548
|
+
}, gobj);
|
|
26549
|
+
};
|
|
26213
26550
|
break;
|
|
26214
26551
|
case "object":
|
|
26215
26552
|
case "dict":
|
|
@@ -26480,13 +26817,21 @@ function table__destroy(gobj) {
|
|
|
26480
26817
|
* What a `file` column shows in a table cell: the asset it names.
|
|
26481
26818
|
*
|
|
26482
26819
|
* A sha256 in full is 64 characters of noise in a column, so it is
|
|
26483
|
-
* shortened and the whole of it
|
|
26484
|
-
* blank -- a blank cell and a cell whose asset failed
|
|
26485
|
-
* same, and only one of them is normal.
|
|
26820
|
+
* shortened and the whole of it goes to the popup the click opens. Empty
|
|
26821
|
+
* is SAID, not left blank -- a blank cell and a cell whose asset failed
|
|
26822
|
+
* to load look the same, and only one of them is normal.
|
|
26823
|
+
*
|
|
26824
|
+
* The value arrives already turned into ids joined by ", "
|
|
26825
|
+
* (transform__treedb_value_2_table_value), and an array column names
|
|
26826
|
+
* several: the first is shown and the rest counted.
|
|
26486
26827
|
************************************************************/
|
|
26828
|
+
function file_cell_ids(value) {
|
|
26829
|
+
if (is_array(value)) return yui_asset_ids(value);
|
|
26830
|
+
return String(value || "").split(",").map((s) => s.trim()).filter((s) => !!s).flatMap((s) => yui_asset_ids(s));
|
|
26831
|
+
}
|
|
26487
26832
|
function file_cell_formatter(cell, formatterParams, onRendered) {
|
|
26488
|
-
const
|
|
26489
|
-
if (!
|
|
26833
|
+
const ids = file_cell_ids(cell.getValue());
|
|
26834
|
+
if (!ids.length) return createElement2([
|
|
26490
26835
|
"span",
|
|
26491
26836
|
{
|
|
26492
26837
|
class: "FILE_CELL is-empty",
|
|
@@ -26494,13 +26839,26 @@ function file_cell_formatter(cell, formatterParams, onRendered) {
|
|
|
26494
26839
|
},
|
|
26495
26840
|
"no file"
|
|
26496
26841
|
]);
|
|
26497
|
-
|
|
26842
|
+
const children = [["span", { class: "FILE_CELL_ICON icon yi-eye" }], [
|
|
26843
|
+
"span",
|
|
26844
|
+
{ class: "FILE_CELL_ID" },
|
|
26845
|
+
yui_file_id_label(ids[0])
|
|
26846
|
+
]];
|
|
26847
|
+
if (ids.length > 1) children.push([
|
|
26498
26848
|
"span",
|
|
26849
|
+
{ class: "FILE_CELL_MORE" },
|
|
26850
|
+
`+${ids.length - 1}`
|
|
26851
|
+
]);
|
|
26852
|
+
return createElement2([
|
|
26853
|
+
"a",
|
|
26499
26854
|
{
|
|
26500
|
-
class: "FILE_CELL",
|
|
26501
|
-
title:
|
|
26855
|
+
class: "FILE_CELL FILE_CELL_LINK",
|
|
26856
|
+
title: t("show file"),
|
|
26857
|
+
"data-i18n-title": "show file",
|
|
26858
|
+
"aria-label": t("show file"),
|
|
26859
|
+
"data-i18n-aria-label": "show file"
|
|
26502
26860
|
},
|
|
26503
|
-
|
|
26861
|
+
children
|
|
26504
26862
|
]);
|
|
26505
26863
|
}
|
|
26506
26864
|
function icon_formatter(cell) {
|
|
@@ -26764,6 +27122,7 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
26764
27122
|
});
|
|
26765
27123
|
gobj_start(form);
|
|
26766
27124
|
refresh_language($body, t);
|
|
27125
|
+
request_form_previews(gobj, record);
|
|
26767
27126
|
let $with_focus = $body.querySelector(".with-focus:not([readonly])");
|
|
26768
27127
|
if (!$with_focus) $with_focus = $body.querySelector("input.input:not([readonly]), textarea, select");
|
|
26769
27128
|
if ($with_focus) $with_focus.focus();
|
|
@@ -27011,6 +27370,225 @@ function close_cell_json_dialog(gobj) {
|
|
|
27011
27370
|
teardown_cell_json_child(gobj);
|
|
27012
27371
|
}
|
|
27013
27372
|
/************************************************************
|
|
27373
|
+
* Ask the host for the bytes of ONE asset.
|
|
27374
|
+
*
|
|
27375
|
+
* Same contract as the page requests: the transport belongs to the
|
|
27376
|
+
* HOST, so the request goes up as an event and the answer comes back
|
|
27377
|
+
* down as one, correlated by a `req_id` the host echoes. What is parked
|
|
27378
|
+
* here is WHERE the answer goes -- the form's preview of a column, or a
|
|
27379
|
+
* slot of the file popup -- and the watchdog, because a request whose
|
|
27380
|
+
* answer never lands would leave a "loading" on screen for ever.
|
|
27381
|
+
*
|
|
27382
|
+
* `target` is kept here and never travels: it holds a gobj and a DOM
|
|
27383
|
+
* node, and a kw is plain json.
|
|
27384
|
+
************************************************************/
|
|
27385
|
+
var ASSET_TIMEOUT_MS = 3e4;
|
|
27386
|
+
function request_asset(gobj, asset_id, target) {
|
|
27387
|
+
let priv = gobj.priv;
|
|
27388
|
+
if (!priv._pending_assets) priv._pending_assets = {};
|
|
27389
|
+
let req_id = `a${++priv._asset_seq}`;
|
|
27390
|
+
let timer = window.setTimeout(function() {
|
|
27391
|
+
gobj_send_event(gobj, "EV_ASSET_FAILED", {
|
|
27392
|
+
req_id,
|
|
27393
|
+
error: "asset request timed out"
|
|
27394
|
+
}, gobj);
|
|
27395
|
+
}, ASSET_TIMEOUT_MS);
|
|
27396
|
+
priv._pending_assets[req_id] = Object.assign({
|
|
27397
|
+
id: asset_id,
|
|
27398
|
+
timer
|
|
27399
|
+
}, target);
|
|
27400
|
+
gobj_publish_event(gobj, "EV_REQUEST_ASSET", {
|
|
27401
|
+
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
27402
|
+
req_id,
|
|
27403
|
+
asset_id
|
|
27404
|
+
});
|
|
27405
|
+
}
|
|
27406
|
+
/************************************************************
|
|
27407
|
+
* Take a parked asset request out, however it ended. Null when
|
|
27408
|
+
* it was already settled (a late answer after a timeout).
|
|
27409
|
+
************************************************************/
|
|
27410
|
+
function take_pending_asset(gobj, req_id) {
|
|
27411
|
+
let priv = gobj.priv;
|
|
27412
|
+
let pend = priv._pending_assets ? priv._pending_assets[req_id] : null;
|
|
27413
|
+
if (!pend) return null;
|
|
27414
|
+
delete priv._pending_assets[req_id];
|
|
27415
|
+
if (pend.timer) window.clearTimeout(pend.timer);
|
|
27416
|
+
return pend;
|
|
27417
|
+
}
|
|
27418
|
+
/************************************************************
|
|
27419
|
+
* Forget every parked asset request: the view is going away,
|
|
27420
|
+
* and a watchdog firing into a stopped gobj is an error for
|
|
27421
|
+
* nothing.
|
|
27422
|
+
************************************************************/
|
|
27423
|
+
function drop_pending_assets(gobj) {
|
|
27424
|
+
let priv = gobj.priv;
|
|
27425
|
+
if (priv._pending_assets) {
|
|
27426
|
+
for (const pend of Object.values(priv._pending_assets)) if (pend.timer) window.clearTimeout(pend.timer);
|
|
27427
|
+
}
|
|
27428
|
+
priv._pending_assets = null;
|
|
27429
|
+
}
|
|
27430
|
+
/************************************************************
|
|
27431
|
+
* Ask for the asset of every `file` column the record names,
|
|
27432
|
+
* for the form's preview. One per column: the form shows the
|
|
27433
|
+
* asset the column keeps, which is the first it names.
|
|
27434
|
+
************************************************************/
|
|
27435
|
+
function request_form_previews(gobj, record) {
|
|
27436
|
+
let priv = gobj.priv;
|
|
27437
|
+
if (!record || !priv.form) return;
|
|
27438
|
+
let desc = gobj_read_attr(gobj, "desc");
|
|
27439
|
+
for (let col of desc.cols) {
|
|
27440
|
+
if (!col.id || col.id.charAt(0) === "_") continue;
|
|
27441
|
+
const field_desc = treedb_get_field_desc(col);
|
|
27442
|
+
if (field_desc.type !== "file" || field_desc.is_hidden) continue;
|
|
27443
|
+
let ids = file_cell_ids(record[col.id]);
|
|
27444
|
+
if (!ids.length) continue;
|
|
27445
|
+
request_asset(gobj, ids[0], {
|
|
27446
|
+
target: "form",
|
|
27447
|
+
form: priv.form,
|
|
27448
|
+
col: col.id
|
|
27449
|
+
});
|
|
27450
|
+
}
|
|
27451
|
+
}
|
|
27452
|
+
/************************************************************
|
|
27453
|
+
* Show what a `file` cell names -- every asset, when the
|
|
27454
|
+
* column is an array -- in the standardized adaptive dialog.
|
|
27455
|
+
*
|
|
27456
|
+
* Unlike the JSON cell this is NOT offline: the row holds the
|
|
27457
|
+
* ids and the bytes are the backend's, so each slot opens as
|
|
27458
|
+
* "loading" and one `get-asset` per id fills it.
|
|
27459
|
+
************************************************************/
|
|
27460
|
+
function open_cell_file_dialog(gobj, row_id, col_id) {
|
|
27461
|
+
close_cell_file_dialog(gobj);
|
|
27462
|
+
let priv = gobj.priv;
|
|
27463
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
27464
|
+
if (!tabulator) {
|
|
27465
|
+
log_error(`${gobj_short_name(gobj)}: no table, no file cell to show`);
|
|
27466
|
+
return;
|
|
27467
|
+
}
|
|
27468
|
+
let row = tabulator.getRow(row_id);
|
|
27469
|
+
if (!row) {
|
|
27470
|
+
log_error(`${gobj_short_name(gobj)}: row '${row_id}' not found in the table`);
|
|
27471
|
+
return;
|
|
27472
|
+
}
|
|
27473
|
+
let ids = file_cell_ids(row.getData()[col_id]);
|
|
27474
|
+
if (!ids.length) {
|
|
27475
|
+
log_error(`${gobj_short_name(gobj)}: row '${row_id}' col '${col_id}' names no file`);
|
|
27476
|
+
return;
|
|
27477
|
+
}
|
|
27478
|
+
let $items = ids.map((id) => createElement2([
|
|
27479
|
+
"div",
|
|
27480
|
+
{ class: "CELL_FILE_ITEM" },
|
|
27481
|
+
[[
|
|
27482
|
+
"div",
|
|
27483
|
+
{ class: "CELL_FILE_SLOT" },
|
|
27484
|
+
[[
|
|
27485
|
+
"span",
|
|
27486
|
+
{
|
|
27487
|
+
class: "CELL_FILE_LOADING",
|
|
27488
|
+
i18n: "loading"
|
|
27489
|
+
},
|
|
27490
|
+
"loading"
|
|
27491
|
+
]]
|
|
27492
|
+
], [
|
|
27493
|
+
"div",
|
|
27494
|
+
{ class: "CELL_FILE_CAPTION" },
|
|
27495
|
+
[[
|
|
27496
|
+
"span",
|
|
27497
|
+
{ class: "CELL_FILE_ID" },
|
|
27498
|
+
id
|
|
27499
|
+
]]
|
|
27500
|
+
]]
|
|
27501
|
+
]));
|
|
27502
|
+
let $box = createElement2([
|
|
27503
|
+
"div",
|
|
27504
|
+
{ class: "TREEDB_CELL_FILE" },
|
|
27505
|
+
$items
|
|
27506
|
+
]);
|
|
27507
|
+
let presented = present_json_popup(gobj, $box, {
|
|
27508
|
+
name: "filewin_" + clean_name(gobj_name(gobj)),
|
|
27509
|
+
logical_class: "TREEDB_CELL_FILE",
|
|
27510
|
+
title_prefix: String(row_id),
|
|
27511
|
+
title: col_id,
|
|
27512
|
+
icon: "yi-eye",
|
|
27513
|
+
forget_window: function() {
|
|
27514
|
+
priv.cell_file_win = null;
|
|
27515
|
+
},
|
|
27516
|
+
on_close: function() {
|
|
27517
|
+
teardown_cell_file(gobj);
|
|
27518
|
+
}
|
|
27519
|
+
});
|
|
27520
|
+
if (!presented) return;
|
|
27521
|
+
priv.cell_file_box = $box;
|
|
27522
|
+
priv.cell_file_modal = presented.modal;
|
|
27523
|
+
priv.cell_file_win = presented.win;
|
|
27524
|
+
ids.forEach((id, i) => {
|
|
27525
|
+
request_asset(gobj, id, {
|
|
27526
|
+
target: "popup",
|
|
27527
|
+
box: $box,
|
|
27528
|
+
slot: i
|
|
27529
|
+
});
|
|
27530
|
+
});
|
|
27531
|
+
}
|
|
27532
|
+
/************************************************************
|
|
27533
|
+
* Fill one slot of the file popup with what came back.
|
|
27534
|
+
************************************************************/
|
|
27535
|
+
function fill_cell_file_slot(gobj, pend, answer, error) {
|
|
27536
|
+
let priv = gobj.priv;
|
|
27537
|
+
if (!priv.cell_file_box || priv.cell_file_box !== pend.box) return;
|
|
27538
|
+
let $item = pend.box.querySelectorAll(".CELL_FILE_ITEM")[pend.slot];
|
|
27539
|
+
let $slot = $item ? $item.querySelector(".CELL_FILE_SLOT") : null;
|
|
27540
|
+
let $caption = $item ? $item.querySelector(".CELL_FILE_CAPTION") : null;
|
|
27541
|
+
if (!$slot || !$caption) {
|
|
27542
|
+
log_error(`${gobj_short_name(gobj)}: file popup has no slot ${pend.slot}`);
|
|
27543
|
+
return;
|
|
27544
|
+
}
|
|
27545
|
+
yui_asset_release($slot);
|
|
27546
|
+
$slot.replaceChildren(yui_asset_element(error ? null : answer, {
|
|
27547
|
+
detail: error || pend.id,
|
|
27548
|
+
alt: pend.id,
|
|
27549
|
+
class: "CELL_FILE_MEDIA"
|
|
27550
|
+
}));
|
|
27551
|
+
refresh_language($slot, t);
|
|
27552
|
+
if (!error && answer) {
|
|
27553
|
+
let meta = [answer.content_type, yui_file_size_label(answer.size)].filter((s) => !!s).join(" · ");
|
|
27554
|
+
if (meta) $caption.appendChild(createElement2([
|
|
27555
|
+
"span",
|
|
27556
|
+
{ class: "CELL_FILE_META" },
|
|
27557
|
+
meta
|
|
27558
|
+
]));
|
|
27559
|
+
let $open = yui_asset_open_link(answer);
|
|
27560
|
+
if ($open) $caption.appendChild($open);
|
|
27561
|
+
}
|
|
27562
|
+
}
|
|
27563
|
+
/************************************************************
|
|
27564
|
+
* Retire the file popup. Called from its on_close -- the
|
|
27565
|
+
* shell or the window has already removed the DOM. The blob
|
|
27566
|
+
* urls it made are revoked; answers still in flight for it
|
|
27567
|
+
* land on nobody (fill_cell_file_slot checks the box).
|
|
27568
|
+
************************************************************/
|
|
27569
|
+
function teardown_cell_file(gobj) {
|
|
27570
|
+
let priv = gobj.priv;
|
|
27571
|
+
yui_asset_release(priv.cell_file_box);
|
|
27572
|
+
priv.cell_file_box = null;
|
|
27573
|
+
let win = priv.cell_file_win;
|
|
27574
|
+
priv.cell_file_win = null;
|
|
27575
|
+
retire_popup_window(win);
|
|
27576
|
+
priv.cell_file_modal = null;
|
|
27577
|
+
}
|
|
27578
|
+
/************************************************************
|
|
27579
|
+
* Close the file popup (teardown, or a second open).
|
|
27580
|
+
************************************************************/
|
|
27581
|
+
function close_cell_file_dialog(gobj) {
|
|
27582
|
+
let priv = gobj.priv;
|
|
27583
|
+
if (priv.cell_file_modal) {
|
|
27584
|
+
let modal = priv.cell_file_modal;
|
|
27585
|
+
priv.cell_file_modal = null;
|
|
27586
|
+
modal.close();
|
|
27587
|
+
return;
|
|
27588
|
+
}
|
|
27589
|
+
teardown_cell_file(gobj);
|
|
27590
|
+
}
|
|
27591
|
+
/************************************************************
|
|
27014
27592
|
* True if the topic's pkey col carries the "rowid" flag.
|
|
27015
27593
|
************************************************************/
|
|
27016
27594
|
function pkey_is_rowid(gobj) {
|
|
@@ -27794,6 +28372,57 @@ function ac_show_cell_json(gobj, event, kw, src) {
|
|
|
27794
28372
|
return 0;
|
|
27795
28373
|
}
|
|
27796
28374
|
/************************************************************
|
|
28375
|
+
* Show the asset(s) one `file` cell names
|
|
28376
|
+
* {
|
|
28377
|
+
* row_id:
|
|
28378
|
+
* col_id:
|
|
28379
|
+
* }
|
|
28380
|
+
************************************************************/
|
|
28381
|
+
function ac_show_cell_file(gobj, event, kw, src) {
|
|
28382
|
+
open_cell_file_dialog(gobj, kw.row_id, kw.col_id);
|
|
28383
|
+
return 0;
|
|
28384
|
+
}
|
|
28385
|
+
/************************************************************
|
|
28386
|
+
* Hand an asset answer -- or why there is none -- to whoever
|
|
28387
|
+
* asked for it, if it is still there to take it.
|
|
28388
|
+
************************************************************/
|
|
28389
|
+
function deliver_asset(gobj, pend, answer, error) {
|
|
28390
|
+
let priv = gobj.priv;
|
|
28391
|
+
if (pend.target === "form") {
|
|
28392
|
+
if (!priv.form || priv.form !== pend.form) return;
|
|
28393
|
+
gobj_send_event(priv.form, "EV_SET_FILE_PREVIEW", {
|
|
28394
|
+
name: pend.col,
|
|
28395
|
+
id: pend.id,
|
|
28396
|
+
answer,
|
|
28397
|
+
error
|
|
28398
|
+
}, gobj);
|
|
28399
|
+
return;
|
|
28400
|
+
}
|
|
28401
|
+
fill_cell_file_slot(gobj, pend, answer, error);
|
|
28402
|
+
}
|
|
28403
|
+
/************************************************************
|
|
28404
|
+
* The host answered an asset.
|
|
28405
|
+
* {req_id, answer}
|
|
28406
|
+
************************************************************/
|
|
28407
|
+
function ac_asset_loaded(gobj, event, kw, src) {
|
|
28408
|
+
let pend = take_pending_asset(gobj, kw.req_id);
|
|
28409
|
+
if (!pend) return 0;
|
|
28410
|
+
deliver_asset(gobj, pend, kw.answer || null, "");
|
|
28411
|
+
return 0;
|
|
28412
|
+
}
|
|
28413
|
+
/************************************************************
|
|
28414
|
+
* The host could not answer it, or nobody did in time.
|
|
28415
|
+
* {req_id, error}
|
|
28416
|
+
************************************************************/
|
|
28417
|
+
function ac_asset_failed(gobj, event, kw, src) {
|
|
28418
|
+
let why = kw && kw.error || "asset request failed";
|
|
28419
|
+
let pend = take_pending_asset(gobj, kw.req_id);
|
|
28420
|
+
log_error(`${gobj_short_name(gobj)}: asset '${pend ? pend.id : kw.req_id}': ${why}`);
|
|
28421
|
+
if (!pend) return 0;
|
|
28422
|
+
deliver_asset(gobj, pend, null, why);
|
|
28423
|
+
return 0;
|
|
28424
|
+
}
|
|
28425
|
+
/************************************************************
|
|
27797
28426
|
* One cell of one record changed. Publish it UP as a field write;
|
|
27798
28427
|
* the host owns the treedb command and its options.
|
|
27799
28428
|
************************************************************/
|
|
@@ -28297,6 +28926,21 @@ function create_gclass$5(gclass_name) {
|
|
|
28297
28926
|
ac_show_cell_json,
|
|
28298
28927
|
null
|
|
28299
28928
|
],
|
|
28929
|
+
[
|
|
28930
|
+
"EV_SHOW_CELL_FILE",
|
|
28931
|
+
ac_show_cell_file,
|
|
28932
|
+
null
|
|
28933
|
+
],
|
|
28934
|
+
[
|
|
28935
|
+
"EV_ASSET_LOADED",
|
|
28936
|
+
ac_asset_loaded,
|
|
28937
|
+
null
|
|
28938
|
+
],
|
|
28939
|
+
[
|
|
28940
|
+
"EV_ASSET_FAILED",
|
|
28941
|
+
ac_asset_failed,
|
|
28942
|
+
null
|
|
28943
|
+
],
|
|
28300
28944
|
[
|
|
28301
28945
|
"EV_CHANGE_LOCALE",
|
|
28302
28946
|
ac_change_locale,
|
|
@@ -28397,6 +29041,10 @@ function create_gclass$5(gclass_name) {
|
|
|
28397
29041
|
["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
|
|
28398
29042
|
["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
|
|
28399
29043
|
["EV_SHOW_CELL_JSON", 0],
|
|
29044
|
+
["EV_SHOW_CELL_FILE", 0],
|
|
29045
|
+
["EV_ASSET_LOADED", 0],
|
|
29046
|
+
["EV_ASSET_FAILED", 0],
|
|
29047
|
+
["EV_REQUEST_ASSET", event_flag_t.EVF_OUTPUT_EVENT],
|
|
28400
29048
|
["EV_CHANGE_LOCALE", 0],
|
|
28401
29049
|
["EV_REFRESH", 0],
|
|
28402
29050
|
["EV_SHOW_SCHEMA", 0],
|