@yuneta/gobj-ui 7.23.157 → 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.
@@ -6095,6 +6095,299 @@ function yui_tabulator_relocalize(table, t) {
6095
6095
  }
6096
6096
  }
6097
6097
  //#endregion
6098
+ //#region src/yui_asset.js
6099
+ var BLOB_ATTR = "data-yui-blob";
6100
+ function ref_to_id(ref) {
6101
+ if (!ref) return null;
6102
+ if (typeof ref === "string") {
6103
+ if (ref.indexOf("^") < 0) return ref;
6104
+ const parts = ref.split("^");
6105
+ return parts.length >= 2 && parts[1] ? parts[1] : null;
6106
+ }
6107
+ if (typeof ref === "object" && typeof ref.id === "string") return ref.id;
6108
+ return null;
6109
+ }
6110
+ /***************************************************************************
6111
+ * Every asset id a column names, in order. Always an array, possibly empty.
6112
+ ***************************************************************************/
6113
+ function yui_asset_ids(ref) {
6114
+ if (Array.isArray(ref)) return ref.map(ref_to_id).filter((id) => !!id);
6115
+ const id = ref_to_id(ref);
6116
+ return id ? [id] : [];
6117
+ }
6118
+ /***************************************************************************
6119
+ * The first asset id a column names, or null.
6120
+ ***************************************************************************/
6121
+ function yui_asset_id(ref) {
6122
+ const ids = yui_asset_ids(ref);
6123
+ return ids.length ? ids[0] : null;
6124
+ }
6125
+ /***************************************************************************
6126
+ * Turn a `get-asset` answer into something an element can load.
6127
+ *
6128
+ * Returns null when the answer carries neither -- which the caller must
6129
+ * treat as "missing", never as an empty src: an <img src=""> reloads the
6130
+ * PAGE in some browsers.
6131
+ ***************************************************************************/
6132
+ function yui_asset_src(answer) {
6133
+ if (!answer) return null;
6134
+ if (answer.mode === "url" && answer.url) return answer.url;
6135
+ if (answer.mode === "inline" && answer.content64) return `data:${answer.content_type || "application/octet-stream"};base64,${answer.content64}`;
6136
+ return null;
6137
+ }
6138
+ /***************************************************************************
6139
+ * The marker for an asset that is not there.
6140
+ *
6141
+ * A missing image used to leave a broken box and no word about it, which
6142
+ * is indistinguishable from a slow one and from a bug. This says so, and
6143
+ * it says WHICH: `detail` is data (a name, a path) and is never
6144
+ * translated, while the label carries its i18n key so a language change
6145
+ * re-translates it.
6146
+ ***************************************************************************/
6147
+ function yui_asset_missing(detail, opts = {}) {
6148
+ const key = opts.key || "asset not available";
6149
+ const children = [["span", { class: "ASSET_MISSING_ICON yi-triangle-exclamation" }], [
6150
+ "span",
6151
+ {
6152
+ class: "ASSET_MISSING_LABEL",
6153
+ i18n: key
6154
+ },
6155
+ key
6156
+ ]];
6157
+ if (detail) children.push([
6158
+ "span",
6159
+ { class: "ASSET_MISSING_DETAIL" },
6160
+ String(detail)
6161
+ ]);
6162
+ const attrs = { class: `ASSET_MISSING ${opts.class || ""}`.trim() };
6163
+ if (detail) attrs.title = String(detail);
6164
+ else attrs["data-i18n-title"] = key;
6165
+ return (0, _yuneta_gobj_js.createElement2)([
6166
+ "div",
6167
+ attrs,
6168
+ children
6169
+ ]);
6170
+ }
6171
+ /***************************************************************************
6172
+ * What KIND of thing a content type is, as far as showing it goes:
6173
+ *
6174
+ * "image" | "video" | "audio" the browser draws it in an element
6175
+ * "pdf" the browser's own viewer, in a frame
6176
+ * "other" nothing here can draw it: a card
6177
+ * saying what it is, and a way to open
6178
+ *
6179
+ * The content type is the one the BACKEND stored (treedb checks it
6180
+ * against the bytes), never the extension of a name a person typed.
6181
+ ***************************************************************************/
6182
+ function yui_asset_kind(content_type) {
6183
+ const ct = String(content_type || "").toLowerCase().split(";")[0].trim();
6184
+ if (ct.startsWith("image/")) return "image";
6185
+ if (ct.startsWith("video/")) return "video";
6186
+ if (ct.startsWith("audio/")) return "audio";
6187
+ if (ct === "application/pdf") return "pdf";
6188
+ return "other";
6189
+ }
6190
+ /***************************************************************************
6191
+ * A `blob:` url for inline bytes, or null.
6192
+ *
6193
+ * A frame and a new tab need a blob and not a data url: Firefox refuses to
6194
+ * NAVIGATE to a data url (a new tab opened on one stays blank), and its
6195
+ * PDF viewer does not run on one inside a frame.
6196
+ ***************************************************************************/
6197
+ function base64_to_blob_url(content64, content_type) {
6198
+ try {
6199
+ const binary = atob(content64);
6200
+ const bytes = new Uint8Array(binary.length);
6201
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
6202
+ const blob = new Blob([bytes], { type: content_type || "application/octet-stream" });
6203
+ return URL.createObjectURL(blob);
6204
+ } catch (e) {
6205
+ (0, _yuneta_gobj_js.log_error)(`cannot decode the bytes of an asset: ${e && e.message ? e.message : e}`);
6206
+ return null;
6207
+ }
6208
+ }
6209
+ /***************************************************************************
6210
+ * Where a frame or a link can take an asset:
6211
+ *
6212
+ * -> {href, blob} `blob` true when href is a blob url THIS call
6213
+ * created, which the caller must mark (BLOB_ATTR)
6214
+ * so that yui_asset_release() revokes it
6215
+ * -> null nothing to open
6216
+ ***************************************************************************/
6217
+ function yui_asset_href(answer) {
6218
+ if (!answer) return null;
6219
+ if (answer.mode === "url" && answer.url) return {
6220
+ href: answer.url,
6221
+ blob: false
6222
+ };
6223
+ if (answer.mode === "inline" && answer.content64) {
6224
+ const href = base64_to_blob_url(answer.content64, answer.content_type);
6225
+ return href ? {
6226
+ href,
6227
+ blob: true
6228
+ } : null;
6229
+ }
6230
+ return null;
6231
+ }
6232
+ /***************************************************************************
6233
+ * The answer a picked `File` would have had from the backend.
6234
+ *
6235
+ * A file a person picked and has not saved yet is shown BEFORE it exists
6236
+ * as an asset, so it goes through the same element as a stored one: a
6237
+ * `url` answer whose url is a blob url over the File itself -- which
6238
+ * reads nothing until the browser draws it.
6239
+ *
6240
+ * `blob: true` says the url is owned: whoever drops the element revokes
6241
+ * it with yui_asset_release().
6242
+ ***************************************************************************/
6243
+ function yui_asset_file_answer(file) {
6244
+ if (!file) return null;
6245
+ let url;
6246
+ try {
6247
+ url = URL.createObjectURL(file);
6248
+ } catch (e) {
6249
+ (0, _yuneta_gobj_js.log_error)(`cannot preview the picked file: ${e && e.message ? e.message : e}`);
6250
+ return null;
6251
+ }
6252
+ return {
6253
+ mode: "url",
6254
+ url,
6255
+ content_type: file.type || "",
6256
+ size: file.size,
6257
+ original_name: file.name || "",
6258
+ blob: true
6259
+ };
6260
+ }
6261
+ /***************************************************************************
6262
+ * Revoke every blob url this module created under `$root`, $root included.
6263
+ *
6264
+ * Call it on whatever element is about to be dropped: a preview being
6265
+ * replaced, a dialog being closed. A blob url that is never revoked keeps
6266
+ * its bytes in memory for the life of the page.
6267
+ ***************************************************************************/
6268
+ function yui_asset_release($root) {
6269
+ if (!$root || !$root.querySelectorAll) return;
6270
+ const marked = Array.from($root.querySelectorAll(`[${BLOB_ATTR}]`));
6271
+ if ($root.hasAttribute && $root.hasAttribute(BLOB_ATTR)) marked.push($root);
6272
+ marked.forEach(($el) => {
6273
+ const url = $el.getAttribute(BLOB_ATTR);
6274
+ $el.removeAttribute(BLOB_ATTR);
6275
+ if (url) URL.revokeObjectURL(url);
6276
+ });
6277
+ }
6278
+ /***************************************************************************
6279
+ * The link that opens an asset in a new tab, or null when there is
6280
+ * nothing to open. A control: named for the eye and for a reader, both
6281
+ * through their keys so a language change re-translates them.
6282
+ ***************************************************************************/
6283
+ function yui_asset_open_link(answer, opts = {}) {
6284
+ const target = yui_asset_href(answer);
6285
+ if (!target) return null;
6286
+ const key = "open in a new tab";
6287
+ const $a = (0, _yuneta_gobj_js.createElement2)([
6288
+ "a",
6289
+ {
6290
+ class: `ASSET_OPEN ${opts.class || ""}`.trim(),
6291
+ target: "_blank",
6292
+ rel: "noopener",
6293
+ title: (0, i18next.t)(key),
6294
+ "data-i18n-title": key,
6295
+ "aria-label": (0, i18next.t)(key),
6296
+ "data-i18n-aria-label": key
6297
+ },
6298
+ [["span", { class: "ASSET_OPEN_ICON yi-link" }], [
6299
+ "span",
6300
+ {
6301
+ class: "ASSET_OPEN_LABEL",
6302
+ i18n: key
6303
+ },
6304
+ key
6305
+ ]]
6306
+ ]);
6307
+ $a.href = target.href;
6308
+ if (target.blob) $a.setAttribute(BLOB_ATTR, target.href);
6309
+ return $a;
6310
+ }
6311
+ /***************************************************************************
6312
+ * The element for one asset.
6313
+ *
6314
+ * The KIND comes from the content type the backend stored, not from the
6315
+ * name a person typed: video and audio are assets too, and an <img> whose
6316
+ * src is a film shows the broken box this module exists to remove.
6317
+ *
6318
+ * `detail` is what the missing marker shows -- pass the original name or
6319
+ * the source path, the thing a person can act on.
6320
+ *
6321
+ * A PDF goes in a frame, where the browser's own viewer draws it. Any
6322
+ * other kind gets a card: what it is, and a link to open it.
6323
+ * TODO: draw more kinds in place (plain text, office documents); today
6324
+ * they are only opened in a new tab, where the browser does what it can.
6325
+ ***************************************************************************/
6326
+ function yui_asset_element(answer, opts = {}) {
6327
+ const src = yui_asset_src(answer);
6328
+ if (!src) return yui_asset_missing(opts.detail, opts);
6329
+ const kind = yui_asset_kind(answer.content_type);
6330
+ const cls = `ASSET_MEDIA ${opts.class || ""}`.trim();
6331
+ if (kind === "pdf") {
6332
+ const target = yui_asset_href(answer);
6333
+ if (!target) return yui_asset_missing(opts.detail, opts);
6334
+ const $frame = (0, _yuneta_gobj_js.createElement2)(["iframe", {
6335
+ class: `${cls} ASSET_DOCUMENT`,
6336
+ title: opts.detail || answer.content_type || ""
6337
+ }]);
6338
+ $frame.src = target.href;
6339
+ if (target.blob) $frame.setAttribute(BLOB_ATTR, target.href);
6340
+ return $frame;
6341
+ }
6342
+ if (kind === "other") {
6343
+ const children = [[
6344
+ "span",
6345
+ { class: "ASSET_OTHER_TYPE" },
6346
+ String(answer.content_type || "?")
6347
+ ]];
6348
+ if (opts.detail) children.push([
6349
+ "span",
6350
+ { class: "ASSET_OTHER_DETAIL" },
6351
+ String(opts.detail)
6352
+ ]);
6353
+ const $card = (0, _yuneta_gobj_js.createElement2)([
6354
+ "div",
6355
+ { class: `${cls} ASSET_OTHER` },
6356
+ children
6357
+ ]);
6358
+ const $open = yui_asset_open_link(answer);
6359
+ if ($open) $card.appendChild($open);
6360
+ return $card;
6361
+ }
6362
+ let el;
6363
+ if (kind === "video") el = (0, _yuneta_gobj_js.createElement2)(["video", {
6364
+ class: cls,
6365
+ controls: "",
6366
+ preload: "metadata"
6367
+ }]);
6368
+ else if (kind === "audio") el = (0, _yuneta_gobj_js.createElement2)(["audio", {
6369
+ class: cls,
6370
+ controls: "",
6371
+ preload: "metadata"
6372
+ }]);
6373
+ else {
6374
+ el = (0, _yuneta_gobj_js.createElement2)(["img", {
6375
+ class: cls,
6376
+ loading: "lazy"
6377
+ }]);
6378
+ el.alt = opts.alt || opts.detail || "";
6379
+ }
6380
+ el.src = src;
6381
+ if (answer.blob) el.setAttribute(BLOB_ATTR, src);
6382
+ el.onerror = () => {
6383
+ (0, _yuneta_gobj_js.log_error)(`asset could not be loaded: ${opts.detail || src}`);
6384
+ const $marker = yui_asset_missing(opts.detail, opts);
6385
+ if (el.parentNode) el.parentNode.replaceChild($marker, el);
6386
+ yui_asset_release(el);
6387
+ };
6388
+ return el;
6389
+ }
6390
+ //#endregion
6098
6391
  //#region src/yui_file_field.js
6099
6392
  /***********************************************************************
6100
6393
  * yui_file_field.js
@@ -6331,6 +6624,10 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
6331
6624
  const $name = (0, _yuneta_gobj_js.createElement2)(["span", { class: "FILE_NAME" }]);
6332
6625
  const $size = (0, _yuneta_gobj_js.createElement2)(["span", { class: "FILE_SIZE" }]);
6333
6626
  const $state = (0, _yuneta_gobj_js.createElement2)(["span", { class: "FILE_STATE" }]);
6627
+ const $preview = (0, _yuneta_gobj_js.createElement2)(["div", {
6628
+ class: "FILE_PREVIEW",
6629
+ hidden: ""
6630
+ }]);
6334
6631
  const $control = (0, _yuneta_gobj_js.createElement2)([
6335
6632
  "div",
6336
6633
  { class: "FILE_FIELD" },
@@ -6340,11 +6637,13 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
6340
6637
  $name,
6341
6638
  $size,
6342
6639
  $state,
6343
- $clear
6640
+ $clear,
6641
+ $preview
6344
6642
  ]
6345
6643
  ]);
6346
6644
  $control.yui_file = null;
6347
6645
  $control.yui_file_value = value || "";
6646
+ $control.yui_file_stored = null;
6348
6647
  const render = () => {
6349
6648
  const picked = $control.yui_file;
6350
6649
  const kept = $control.yui_file_value;
@@ -6365,6 +6664,24 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
6365
6664
  }
6366
6665
  $clear.hidden = readonly || !picked && !kept;
6367
6666
  $pick.hidden = !!readonly;
6667
+ yui_asset_release($preview);
6668
+ $preview.replaceChildren();
6669
+ let $media = null;
6670
+ if (picked) {
6671
+ const answer = yui_asset_file_answer(picked);
6672
+ if (answer) $media = yui_asset_element(answer, {
6673
+ detail: picked.name || "",
6674
+ class: "FILE_PREVIEW_MEDIA"
6675
+ });
6676
+ } else if (kept) {
6677
+ const stored = $control.yui_file_stored;
6678
+ if (stored && stored.id === kept) $media = yui_asset_element(stored.answer, {
6679
+ detail: stored.error || kept,
6680
+ class: "FILE_PREVIEW_MEDIA"
6681
+ });
6682
+ }
6683
+ if ($media) $preview.appendChild($media);
6684
+ $preview.hidden = !$media;
6368
6685
  };
6369
6686
  if (!readonly) {
6370
6687
  $pick.addEventListener("click", () => {
@@ -6393,120 +6710,6 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
6393
6710
  };
6394
6711
  }
6395
6712
  //#endregion
6396
- //#region src/yui_asset.js
6397
- function ref_to_id(ref) {
6398
- if (!ref) return null;
6399
- if (typeof ref === "string") {
6400
- if (ref.indexOf("^") < 0) return ref;
6401
- const parts = ref.split("^");
6402
- return parts.length >= 2 && parts[1] ? parts[1] : null;
6403
- }
6404
- if (typeof ref === "object" && typeof ref.id === "string") return ref.id;
6405
- return null;
6406
- }
6407
- /***************************************************************************
6408
- * Every asset id a column names, in order. Always an array, possibly empty.
6409
- ***************************************************************************/
6410
- function yui_asset_ids(ref) {
6411
- if (Array.isArray(ref)) return ref.map(ref_to_id).filter((id) => !!id);
6412
- const id = ref_to_id(ref);
6413
- return id ? [id] : [];
6414
- }
6415
- /***************************************************************************
6416
- * The first asset id a column names, or null.
6417
- ***************************************************************************/
6418
- function yui_asset_id(ref) {
6419
- const ids = yui_asset_ids(ref);
6420
- return ids.length ? ids[0] : null;
6421
- }
6422
- /***************************************************************************
6423
- * Turn a `get-asset` answer into something an element can load.
6424
- *
6425
- * Returns null when the answer carries neither -- which the caller must
6426
- * treat as "missing", never as an empty src: an <img src=""> reloads the
6427
- * PAGE in some browsers.
6428
- ***************************************************************************/
6429
- function yui_asset_src(answer) {
6430
- if (!answer) return null;
6431
- if (answer.mode === "url" && answer.url) return answer.url;
6432
- if (answer.mode === "inline" && answer.content64) return `data:${answer.content_type || "application/octet-stream"};base64,${answer.content64}`;
6433
- return null;
6434
- }
6435
- /***************************************************************************
6436
- * The marker for an asset that is not there.
6437
- *
6438
- * A missing image used to leave a broken box and no word about it, which
6439
- * is indistinguishable from a slow one and from a bug. This says so, and
6440
- * it says WHICH: `detail` is data (a name, a path) and is never
6441
- * translated, while the label carries its i18n key so a language change
6442
- * re-translates it.
6443
- ***************************************************************************/
6444
- function yui_asset_missing(detail, opts = {}) {
6445
- const key = opts.key || "asset not available";
6446
- const children = [["span", { class: "ASSET_MISSING_ICON yi-triangle-exclamation" }], [
6447
- "span",
6448
- {
6449
- class: "ASSET_MISSING_LABEL",
6450
- i18n: key
6451
- },
6452
- key
6453
- ]];
6454
- if (detail) children.push([
6455
- "span",
6456
- { class: "ASSET_MISSING_DETAIL" },
6457
- String(detail)
6458
- ]);
6459
- const attrs = { class: `ASSET_MISSING ${opts.class || ""}`.trim() };
6460
- if (detail) attrs.title = String(detail);
6461
- else attrs["data-i18n-title"] = key;
6462
- return (0, _yuneta_gobj_js.createElement2)([
6463
- "div",
6464
- attrs,
6465
- children
6466
- ]);
6467
- }
6468
- /***************************************************************************
6469
- * The element for one asset.
6470
- *
6471
- * The KIND comes from the content type the backend stored, not from the
6472
- * name a person typed: video and audio are assets too, and an <img> whose
6473
- * src is a film shows the broken box this module exists to remove.
6474
- *
6475
- * `detail` is what the missing marker shows -- pass the original name or
6476
- * the source path, the thing a person can act on.
6477
- ***************************************************************************/
6478
- function yui_asset_element(answer, opts = {}) {
6479
- const src = yui_asset_src(answer);
6480
- if (!src) return yui_asset_missing(opts.detail, opts);
6481
- const ct = (answer.content_type || "").toLowerCase();
6482
- const cls = `ASSET_MEDIA ${opts.class || ""}`.trim();
6483
- let el;
6484
- if (ct.startsWith("video/")) el = (0, _yuneta_gobj_js.createElement2)(["video", {
6485
- class: cls,
6486
- controls: "",
6487
- preload: "metadata"
6488
- }]);
6489
- else if (ct.startsWith("audio/")) el = (0, _yuneta_gobj_js.createElement2)(["audio", {
6490
- class: cls,
6491
- controls: "",
6492
- preload: "metadata"
6493
- }]);
6494
- else {
6495
- el = (0, _yuneta_gobj_js.createElement2)(["img", {
6496
- class: cls,
6497
- loading: "lazy"
6498
- }]);
6499
- el.alt = opts.alt || opts.detail || "";
6500
- }
6501
- el.src = src;
6502
- el.onerror = () => {
6503
- (0, _yuneta_gobj_js.log_error)(`asset could not be loaded: ${opts.detail || src}`);
6504
- const $marker = yui_asset_missing(opts.detail, opts);
6505
- if (el.parentNode) el.parentNode.replaceChild($marker, el);
6506
- };
6507
- return el;
6508
- }
6509
- //#endregion
6510
6713
  //#region src/yui_inputs.js
6511
6714
  /***************************************************************
6512
6715
  * The ✕ shows only when there is something to clear AND the field
@@ -7176,6 +7379,7 @@ function mt_create$17(gobj) {
7176
7379
  * and subscriptions are already deleted.
7177
7380
  ************************************************************/
7178
7381
  function mt_destroy$17(gobj) {
7382
+ yui_asset_release((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"));
7179
7383
  destroy_ui$10(gobj);
7180
7384
  }
7181
7385
  /************************************************************
@@ -9324,6 +9528,43 @@ function ac_clear_record(gobj, event, kw, src) {
9324
9528
  return 0;
9325
9529
  }
9326
9530
  /************************************************************
9531
+ * From the host: what it fetched for the asset a `file` column
9532
+ * names -- the `get-asset` answer, or why there is none.
9533
+ * {
9534
+ * name: the column
9535
+ * id: the asset id the answer is for
9536
+ * answer: the get-asset answer, or null
9537
+ * error: why there is no answer, when there is none
9538
+ * }
9539
+ *
9540
+ * Kept on the control and drawn only while the column still names
9541
+ * THAT id: an answer landing after the person picked another file
9542
+ * or cleared the column must not bring the old picture back.
9543
+ ************************************************************/
9544
+ function ac_set_file_preview(gobj, event, kw, src) {
9545
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
9546
+ let $form = $container ? $container.querySelector("form") : null;
9547
+ if (!$form) {
9548
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no form to show the file preview in`);
9549
+ return -1;
9550
+ }
9551
+ let $file = null;
9552
+ $form.querySelectorAll(".FILE_FIELD").forEach(($el) => {
9553
+ if ($el.dataset.treedb_name === kw.name) $file = $el;
9554
+ });
9555
+ if (!$file) {
9556
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no file column '${kw.name}' in the form`);
9557
+ return -1;
9558
+ }
9559
+ $file.yui_file_stored = {
9560
+ id: kw.id || "",
9561
+ answer: kw.answer || null,
9562
+ error: kw.error || ""
9563
+ };
9564
+ if ($file.yui_file_render) $file.yui_file_render();
9565
+ return 0;
9566
+ }
9567
+ /************************************************************
9327
9568
  * From internal form button for new rows
9328
9569
  * or from load_tabulator_data() to load data from backend
9329
9570
  ************************************************************/
@@ -9517,6 +9758,11 @@ function create_gclass$17(gclass_name) {
9517
9758
  ac_paste_record,
9518
9759
  null
9519
9760
  ],
9761
+ [
9762
+ "EV_SET_FILE_PREVIEW",
9763
+ ac_set_file_preview,
9764
+ null
9765
+ ],
9520
9766
  [
9521
9767
  "EV_WINDOW_MOVED",
9522
9768
  null,
@@ -9562,6 +9808,7 @@ function create_gclass$17(gclass_name) {
9562
9808
  ["EV_RECORD_CHANGED", 0],
9563
9809
  ["EV_COPY_RECORD", 0],
9564
9810
  ["EV_PASTE_RECORD", 0],
9811
+ ["EV_SET_FILE_PREVIEW", 0],
9565
9812
  ["EV_WINDOW_MOVED", 0],
9566
9813
  ["EV_WINDOW_RESIZED", 0],
9567
9814
  ["EV_WINDOW_TO_CLOSE", 0],
@@ -22759,6 +23006,7 @@ var attrs_table$6 = [
22759
23006
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
22760
23007
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
22761
23008
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
23009
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
22762
23010
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
22763
23011
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
22764
23012
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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"),
@@ -24078,6 +24326,24 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
24078
24326
  else (0, _yuneta_gobj_js.gobj_send_event)(jv, "EV_SET_JSON", { json: data }, gobj);
24079
24327
  return 0;
24080
24328
  }
24329
+ if (command === "get-asset") {
24330
+ let asset_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
24331
+ let req_id = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "req_id", "", 0);
24332
+ let gobj_topic_form = (0, _yuneta_gobj_js.gobj_find_child)(gobj, { __gobj_name__: `${(0, _yuneta_gobj_js.gobj_name)(gobj)}?${asset_topic}` });
24333
+ if (!gobj_topic_form || !req_id) {
24334
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: get-asset answer for nobody: topic '${asset_topic}', req_id '${req_id}'`);
24335
+ return 0;
24336
+ }
24337
+ if (result < 0) (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_ASSET_FAILED", {
24338
+ req_id,
24339
+ error: comment || "get-asset failed"
24340
+ }, gobj);
24341
+ else (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_ASSET_LOADED", {
24342
+ req_id,
24343
+ answer: data
24344
+ }, gobj);
24345
+ return 0;
24346
+ }
24081
24347
  if (result < 0) {
24082
24348
  if (command === "descs") show_load_error$1(gobj, (0, i18next.t)(comment));
24083
24349
  else {
@@ -24359,6 +24625,48 @@ function ac_request_page(gobj, event, kw, src) {
24359
24625
  return 0;
24360
24626
  }
24361
24627
  /********************************************
24628
+ * Event from formtable: the bytes of one asset a `file` column names,
24629
+ * for the form's preview or the table's file popup.
24630
+ *
24631
+ * Asked to the ASSETS service and not to the treedb: storing is the
24632
+ * treedb's, the way out is C_ASSETS's (a signed url when a web server
24633
+ * sits in front of the store, the bytes inline when there is none). The
24634
+ * answer goes back down to the table that asked, by `req_id`.
24635
+ *
24636
+ * A read: a read-only treedb still answers it.
24637
+ ********************************************/
24638
+ function ac_request_asset(gobj, event, kw, src) {
24639
+ if (!kw.req_id || !kw.asset_id) {
24640
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: an asset request with no id`);
24641
+ return -1;
24642
+ }
24643
+ let assets_service = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "assets_service");
24644
+ let gobj_remote_yuno = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
24645
+ if (!assets_service || !gobj_remote_yuno) {
24646
+ (0, _yuneta_gobj_js.gobj_send_event)(src, "EV_ASSET_FAILED", {
24647
+ req_id: kw.req_id,
24648
+ error: assets_service ? "cannot reach the backend" : "no assets service"
24649
+ }, gobj);
24650
+ return 0;
24651
+ }
24652
+ let ret = (0, _yuneta_gobj_js.gobj_command)(gobj_remote_yuno, "get-asset", {
24653
+ service: assets_service,
24654
+ asset_id: kw.asset_id,
24655
+ __md_command__: {
24656
+ topic_name: kw.topic_name || (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name"),
24657
+ req_id: kw.req_id
24658
+ }
24659
+ }, gobj);
24660
+ if (ret) {
24661
+ (0, _yuneta_gobj_js.log_error)(ret);
24662
+ (0, _yuneta_gobj_js.gobj_send_event)(src, "EV_ASSET_FAILED", {
24663
+ req_id: kw.req_id,
24664
+ error: "cannot reach the backend"
24665
+ }, gobj);
24666
+ }
24667
+ return 0;
24668
+ }
24669
+ /********************************************
24362
24670
  * Event from formtable: ONE field of one record, edited in place.
24363
24671
  *
24364
24672
  * Written as a PARTIAL update and, deliberately, with NO `autolink`.
@@ -24544,6 +24852,11 @@ function create_gclass$6(gclass_name) {
24544
24852
  ac_request_page,
24545
24853
  null
24546
24854
  ],
24855
+ [
24856
+ "EV_REQUEST_ASSET",
24857
+ ac_request_asset,
24858
+ null
24859
+ ],
24547
24860
  [
24548
24861
  "EV_DELETE_RECORD",
24549
24862
  ac_delete_record,
@@ -24624,6 +24937,7 @@ function create_gclass$6(gclass_name) {
24624
24937
  ["EV_UPDATE_RECORD", 0],
24625
24938
  ["EV_UPDATE_FIELD", 0],
24626
24939
  ["EV_REQUEST_PAGE", 0],
24940
+ ["EV_REQUEST_ASSET", 0],
24627
24941
  ["EV_DELETE_RECORD", 0],
24628
24942
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
24629
24943
  ["EV_REFRESH_TOPIC", 0],
@@ -25429,7 +25743,12 @@ var PRIVATE_DATA$5 = {
25429
25743
  schema_win: null,
25430
25744
  cell_json_gobj: null,
25431
25745
  cell_json_modal: null,
25432
- cell_json_win: null
25746
+ cell_json_win: null,
25747
+ cell_file_box: null,
25748
+ cell_file_modal: null,
25749
+ cell_file_win: null,
25750
+ _pending_assets: null,
25751
+ _asset_seq: 0
25433
25752
  };
25434
25753
  var __gclass__$5 = null;
25435
25754
  function is_mobile$1() {
@@ -25562,6 +25881,8 @@ function mt_stop$5(gobj) {
25562
25881
  close_form_dialog(gobj);
25563
25882
  close_schema_dialog(gobj);
25564
25883
  close_cell_json_dialog(gobj);
25884
+ close_cell_file_dialog(gobj);
25885
+ drop_pending_assets(gobj);
25565
25886
  table__destroy(gobj);
25566
25887
  }
25567
25888
  /***************************************************************
@@ -25571,6 +25892,8 @@ function mt_destroy$5(gobj) {
25571
25892
  close_form_dialog(gobj);
25572
25893
  close_schema_dialog(gobj);
25573
25894
  close_cell_json_dialog(gobj);
25895
+ close_cell_file_dialog(gobj);
25896
+ drop_pending_assets(gobj);
25574
25897
  destroy_ui$2(gobj);
25575
25898
  }
25576
25899
  /************************************************************
@@ -26215,6 +26538,20 @@ function create_tabulator(gobj) {
26215
26538
  break;
26216
26539
  case "file":
26217
26540
  colFormatter = file_cell_formatter;
26541
+ cellClick = function(e, cell) {
26542
+ if (!e.target.closest(".FILE_CELL_LINK")) return;
26543
+ e.stopPropagation();
26544
+ let pkey = desc.pkey || "id";
26545
+ let row_id = cell.getData()[pkey];
26546
+ if (row_id === void 0 || row_id === null) {
26547
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: row without pkey '${pkey}', cannot open the file of '${cell.getField()}'`);
26548
+ return;
26549
+ }
26550
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SHOW_CELL_FILE", {
26551
+ row_id,
26552
+ col_id: cell.getField()
26553
+ }, gobj);
26554
+ };
26218
26555
  break;
26219
26556
  case "object":
26220
26557
  case "dict":
@@ -26485,13 +26822,21 @@ function table__destroy(gobj) {
26485
26822
  * What a `file` column shows in a table cell: the asset it names.
26486
26823
  *
26487
26824
  * A sha256 in full is 64 characters of noise in a column, so it is
26488
- * shortened and the whole of it is the tooltip. Empty is SAID, not left
26489
- * blank -- a blank cell and a cell whose asset failed to load look the
26490
- * same, and only one of them is normal.
26825
+ * shortened and the whole of it goes to the popup the click opens. Empty
26826
+ * is SAID, not left blank -- a blank cell and a cell whose asset failed
26827
+ * to load look the same, and only one of them is normal.
26828
+ *
26829
+ * The value arrives already turned into ids joined by ", "
26830
+ * (transform__treedb_value_2_table_value), and an array column names
26831
+ * several: the first is shown and the rest counted.
26491
26832
  ************************************************************/
26833
+ function file_cell_ids(value) {
26834
+ if ((0, _yuneta_gobj_js.is_array)(value)) return yui_asset_ids(value);
26835
+ return String(value || "").split(",").map((s) => s.trim()).filter((s) => !!s).flatMap((s) => yui_asset_ids(s));
26836
+ }
26492
26837
  function file_cell_formatter(cell, formatterParams, onRendered) {
26493
- const id = yui_asset_id(cell.getValue());
26494
- if (!id) return (0, _yuneta_gobj_js.createElement2)([
26838
+ const ids = file_cell_ids(cell.getValue());
26839
+ if (!ids.length) return (0, _yuneta_gobj_js.createElement2)([
26495
26840
  "span",
26496
26841
  {
26497
26842
  class: "FILE_CELL is-empty",
@@ -26499,13 +26844,26 @@ function file_cell_formatter(cell, formatterParams, onRendered) {
26499
26844
  },
26500
26845
  "no file"
26501
26846
  ]);
26502
- return (0, _yuneta_gobj_js.createElement2)([
26847
+ const children = [["span", { class: "FILE_CELL_ICON icon yi-eye" }], [
26848
+ "span",
26849
+ { class: "FILE_CELL_ID" },
26850
+ yui_file_id_label(ids[0])
26851
+ ]];
26852
+ if (ids.length > 1) children.push([
26503
26853
  "span",
26854
+ { class: "FILE_CELL_MORE" },
26855
+ `+${ids.length - 1}`
26856
+ ]);
26857
+ return (0, _yuneta_gobj_js.createElement2)([
26858
+ "a",
26504
26859
  {
26505
- class: "FILE_CELL",
26506
- title: id
26860
+ class: "FILE_CELL FILE_CELL_LINK",
26861
+ title: (0, i18next.t)("show file"),
26862
+ "data-i18n-title": "show file",
26863
+ "aria-label": (0, i18next.t)("show file"),
26864
+ "data-i18n-aria-label": "show file"
26507
26865
  },
26508
- yui_file_id_label(id)
26866
+ children
26509
26867
  ]);
26510
26868
  }
26511
26869
  function icon_formatter(cell) {
@@ -26769,6 +27127,7 @@ function open_form_dialog(gobj, mode, record) {
26769
27127
  });
26770
27128
  (0, _yuneta_gobj_js.gobj_start)(form);
26771
27129
  (0, _yuneta_gobj_js.refresh_language)($body, i18next.t);
27130
+ request_form_previews(gobj, record);
26772
27131
  let $with_focus = $body.querySelector(".with-focus:not([readonly])");
26773
27132
  if (!$with_focus) $with_focus = $body.querySelector("input.input:not([readonly]), textarea, select");
26774
27133
  if ($with_focus) $with_focus.focus();
@@ -27016,6 +27375,225 @@ function close_cell_json_dialog(gobj) {
27016
27375
  teardown_cell_json_child(gobj);
27017
27376
  }
27018
27377
  /************************************************************
27378
+ * Ask the host for the bytes of ONE asset.
27379
+ *
27380
+ * Same contract as the page requests: the transport belongs to the
27381
+ * HOST, so the request goes up as an event and the answer comes back
27382
+ * down as one, correlated by a `req_id` the host echoes. What is parked
27383
+ * here is WHERE the answer goes -- the form's preview of a column, or a
27384
+ * slot of the file popup -- and the watchdog, because a request whose
27385
+ * answer never lands would leave a "loading" on screen for ever.
27386
+ *
27387
+ * `target` is kept here and never travels: it holds a gobj and a DOM
27388
+ * node, and a kw is plain json.
27389
+ ************************************************************/
27390
+ var ASSET_TIMEOUT_MS = 3e4;
27391
+ function request_asset(gobj, asset_id, target) {
27392
+ let priv = gobj.priv;
27393
+ if (!priv._pending_assets) priv._pending_assets = {};
27394
+ let req_id = `a${++priv._asset_seq}`;
27395
+ let timer = window.setTimeout(function() {
27396
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ASSET_FAILED", {
27397
+ req_id,
27398
+ error: "asset request timed out"
27399
+ }, gobj);
27400
+ }, ASSET_TIMEOUT_MS);
27401
+ priv._pending_assets[req_id] = Object.assign({
27402
+ id: asset_id,
27403
+ timer
27404
+ }, target);
27405
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_REQUEST_ASSET", {
27406
+ topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
27407
+ req_id,
27408
+ asset_id
27409
+ });
27410
+ }
27411
+ /************************************************************
27412
+ * Take a parked asset request out, however it ended. Null when
27413
+ * it was already settled (a late answer after a timeout).
27414
+ ************************************************************/
27415
+ function take_pending_asset(gobj, req_id) {
27416
+ let priv = gobj.priv;
27417
+ let pend = priv._pending_assets ? priv._pending_assets[req_id] : null;
27418
+ if (!pend) return null;
27419
+ delete priv._pending_assets[req_id];
27420
+ if (pend.timer) window.clearTimeout(pend.timer);
27421
+ return pend;
27422
+ }
27423
+ /************************************************************
27424
+ * Forget every parked asset request: the view is going away,
27425
+ * and a watchdog firing into a stopped gobj is an error for
27426
+ * nothing.
27427
+ ************************************************************/
27428
+ function drop_pending_assets(gobj) {
27429
+ let priv = gobj.priv;
27430
+ if (priv._pending_assets) {
27431
+ for (const pend of Object.values(priv._pending_assets)) if (pend.timer) window.clearTimeout(pend.timer);
27432
+ }
27433
+ priv._pending_assets = null;
27434
+ }
27435
+ /************************************************************
27436
+ * Ask for the asset of every `file` column the record names,
27437
+ * for the form's preview. One per column: the form shows the
27438
+ * asset the column keeps, which is the first it names.
27439
+ ************************************************************/
27440
+ function request_form_previews(gobj, record) {
27441
+ let priv = gobj.priv;
27442
+ if (!record || !priv.form) return;
27443
+ let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
27444
+ for (let col of desc.cols) {
27445
+ if (!col.id || col.id.charAt(0) === "_") continue;
27446
+ const field_desc = (0, _yuneta_gobj_js.treedb_get_field_desc)(col);
27447
+ if (field_desc.type !== "file" || field_desc.is_hidden) continue;
27448
+ let ids = file_cell_ids(record[col.id]);
27449
+ if (!ids.length) continue;
27450
+ request_asset(gobj, ids[0], {
27451
+ target: "form",
27452
+ form: priv.form,
27453
+ col: col.id
27454
+ });
27455
+ }
27456
+ }
27457
+ /************************************************************
27458
+ * Show what a `file` cell names -- every asset, when the
27459
+ * column is an array -- in the standardized adaptive dialog.
27460
+ *
27461
+ * Unlike the JSON cell this is NOT offline: the row holds the
27462
+ * ids and the bytes are the backend's, so each slot opens as
27463
+ * "loading" and one `get-asset` per id fills it.
27464
+ ************************************************************/
27465
+ function open_cell_file_dialog(gobj, row_id, col_id) {
27466
+ close_cell_file_dialog(gobj);
27467
+ let priv = gobj.priv;
27468
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
27469
+ if (!tabulator) {
27470
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no table, no file cell to show`);
27471
+ return;
27472
+ }
27473
+ let row = tabulator.getRow(row_id);
27474
+ if (!row) {
27475
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: row '${row_id}' not found in the table`);
27476
+ return;
27477
+ }
27478
+ let ids = file_cell_ids(row.getData()[col_id]);
27479
+ if (!ids.length) {
27480
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: row '${row_id}' col '${col_id}' names no file`);
27481
+ return;
27482
+ }
27483
+ let $items = ids.map((id) => (0, _yuneta_gobj_js.createElement2)([
27484
+ "div",
27485
+ { class: "CELL_FILE_ITEM" },
27486
+ [[
27487
+ "div",
27488
+ { class: "CELL_FILE_SLOT" },
27489
+ [[
27490
+ "span",
27491
+ {
27492
+ class: "CELL_FILE_LOADING",
27493
+ i18n: "loading"
27494
+ },
27495
+ "loading"
27496
+ ]]
27497
+ ], [
27498
+ "div",
27499
+ { class: "CELL_FILE_CAPTION" },
27500
+ [[
27501
+ "span",
27502
+ { class: "CELL_FILE_ID" },
27503
+ id
27504
+ ]]
27505
+ ]]
27506
+ ]));
27507
+ let $box = (0, _yuneta_gobj_js.createElement2)([
27508
+ "div",
27509
+ { class: "TREEDB_CELL_FILE" },
27510
+ $items
27511
+ ]);
27512
+ let presented = present_json_popup(gobj, $box, {
27513
+ name: "filewin_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)),
27514
+ logical_class: "TREEDB_CELL_FILE",
27515
+ title_prefix: String(row_id),
27516
+ title: col_id,
27517
+ icon: "yi-eye",
27518
+ forget_window: function() {
27519
+ priv.cell_file_win = null;
27520
+ },
27521
+ on_close: function() {
27522
+ teardown_cell_file(gobj);
27523
+ }
27524
+ });
27525
+ if (!presented) return;
27526
+ priv.cell_file_box = $box;
27527
+ priv.cell_file_modal = presented.modal;
27528
+ priv.cell_file_win = presented.win;
27529
+ ids.forEach((id, i) => {
27530
+ request_asset(gobj, id, {
27531
+ target: "popup",
27532
+ box: $box,
27533
+ slot: i
27534
+ });
27535
+ });
27536
+ }
27537
+ /************************************************************
27538
+ * Fill one slot of the file popup with what came back.
27539
+ ************************************************************/
27540
+ function fill_cell_file_slot(gobj, pend, answer, error) {
27541
+ let priv = gobj.priv;
27542
+ if (!priv.cell_file_box || priv.cell_file_box !== pend.box) return;
27543
+ let $item = pend.box.querySelectorAll(".CELL_FILE_ITEM")[pend.slot];
27544
+ let $slot = $item ? $item.querySelector(".CELL_FILE_SLOT") : null;
27545
+ let $caption = $item ? $item.querySelector(".CELL_FILE_CAPTION") : null;
27546
+ if (!$slot || !$caption) {
27547
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: file popup has no slot ${pend.slot}`);
27548
+ return;
27549
+ }
27550
+ yui_asset_release($slot);
27551
+ $slot.replaceChildren(yui_asset_element(error ? null : answer, {
27552
+ detail: error || pend.id,
27553
+ alt: pend.id,
27554
+ class: "CELL_FILE_MEDIA"
27555
+ }));
27556
+ (0, _yuneta_gobj_js.refresh_language)($slot, i18next.t);
27557
+ if (!error && answer) {
27558
+ let meta = [answer.content_type, yui_file_size_label(answer.size)].filter((s) => !!s).join(" · ");
27559
+ if (meta) $caption.appendChild((0, _yuneta_gobj_js.createElement2)([
27560
+ "span",
27561
+ { class: "CELL_FILE_META" },
27562
+ meta
27563
+ ]));
27564
+ let $open = yui_asset_open_link(answer);
27565
+ if ($open) $caption.appendChild($open);
27566
+ }
27567
+ }
27568
+ /************************************************************
27569
+ * Retire the file popup. Called from its on_close -- the
27570
+ * shell or the window has already removed the DOM. The blob
27571
+ * urls it made are revoked; answers still in flight for it
27572
+ * land on nobody (fill_cell_file_slot checks the box).
27573
+ ************************************************************/
27574
+ function teardown_cell_file(gobj) {
27575
+ let priv = gobj.priv;
27576
+ yui_asset_release(priv.cell_file_box);
27577
+ priv.cell_file_box = null;
27578
+ let win = priv.cell_file_win;
27579
+ priv.cell_file_win = null;
27580
+ retire_popup_window(win);
27581
+ priv.cell_file_modal = null;
27582
+ }
27583
+ /************************************************************
27584
+ * Close the file popup (teardown, or a second open).
27585
+ ************************************************************/
27586
+ function close_cell_file_dialog(gobj) {
27587
+ let priv = gobj.priv;
27588
+ if (priv.cell_file_modal) {
27589
+ let modal = priv.cell_file_modal;
27590
+ priv.cell_file_modal = null;
27591
+ modal.close();
27592
+ return;
27593
+ }
27594
+ teardown_cell_file(gobj);
27595
+ }
27596
+ /************************************************************
27019
27597
  * True if the topic's pkey col carries the "rowid" flag.
27020
27598
  ************************************************************/
27021
27599
  function pkey_is_rowid(gobj) {
@@ -27799,6 +28377,57 @@ function ac_show_cell_json(gobj, event, kw, src) {
27799
28377
  return 0;
27800
28378
  }
27801
28379
  /************************************************************
28380
+ * Show the asset(s) one `file` cell names
28381
+ * {
28382
+ * row_id:
28383
+ * col_id:
28384
+ * }
28385
+ ************************************************************/
28386
+ function ac_show_cell_file(gobj, event, kw, src) {
28387
+ open_cell_file_dialog(gobj, kw.row_id, kw.col_id);
28388
+ return 0;
28389
+ }
28390
+ /************************************************************
28391
+ * Hand an asset answer -- or why there is none -- to whoever
28392
+ * asked for it, if it is still there to take it.
28393
+ ************************************************************/
28394
+ function deliver_asset(gobj, pend, answer, error) {
28395
+ let priv = gobj.priv;
28396
+ if (pend.target === "form") {
28397
+ if (!priv.form || priv.form !== pend.form) return;
28398
+ (0, _yuneta_gobj_js.gobj_send_event)(priv.form, "EV_SET_FILE_PREVIEW", {
28399
+ name: pend.col,
28400
+ id: pend.id,
28401
+ answer,
28402
+ error
28403
+ }, gobj);
28404
+ return;
28405
+ }
28406
+ fill_cell_file_slot(gobj, pend, answer, error);
28407
+ }
28408
+ /************************************************************
28409
+ * The host answered an asset.
28410
+ * {req_id, answer}
28411
+ ************************************************************/
28412
+ function ac_asset_loaded(gobj, event, kw, src) {
28413
+ let pend = take_pending_asset(gobj, kw.req_id);
28414
+ if (!pend) return 0;
28415
+ deliver_asset(gobj, pend, kw.answer || null, "");
28416
+ return 0;
28417
+ }
28418
+ /************************************************************
28419
+ * The host could not answer it, or nobody did in time.
28420
+ * {req_id, error}
28421
+ ************************************************************/
28422
+ function ac_asset_failed(gobj, event, kw, src) {
28423
+ let why = kw && kw.error || "asset request failed";
28424
+ let pend = take_pending_asset(gobj, kw.req_id);
28425
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: asset '${pend ? pend.id : kw.req_id}': ${why}`);
28426
+ if (!pend) return 0;
28427
+ deliver_asset(gobj, pend, null, why);
28428
+ return 0;
28429
+ }
28430
+ /************************************************************
27802
28431
  * One cell of one record changed. Publish it UP as a field write;
27803
28432
  * the host owns the treedb command and its options.
27804
28433
  ************************************************************/
@@ -28302,6 +28931,21 @@ function create_gclass$5(gclass_name) {
28302
28931
  ac_show_cell_json,
28303
28932
  null
28304
28933
  ],
28934
+ [
28935
+ "EV_SHOW_CELL_FILE",
28936
+ ac_show_cell_file,
28937
+ null
28938
+ ],
28939
+ [
28940
+ "EV_ASSET_LOADED",
28941
+ ac_asset_loaded,
28942
+ null
28943
+ ],
28944
+ [
28945
+ "EV_ASSET_FAILED",
28946
+ ac_asset_failed,
28947
+ null
28948
+ ],
28305
28949
  [
28306
28950
  "EV_CHANGE_LOCALE",
28307
28951
  ac_change_locale,
@@ -28402,6 +29046,10 @@ function create_gclass$5(gclass_name) {
28402
29046
  ["EV_UNSELECT_ROWS", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
28403
29047
  ["EV_SHOW_HOOK_DATA", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
28404
29048
  ["EV_SHOW_CELL_JSON", 0],
29049
+ ["EV_SHOW_CELL_FILE", 0],
29050
+ ["EV_ASSET_LOADED", 0],
29051
+ ["EV_ASSET_FAILED", 0],
29052
+ ["EV_REQUEST_ASSET", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
28405
29053
  ["EV_CHANGE_LOCALE", 0],
28406
29054
  ["EV_REFRESH", 0],
28407
29055
  ["EV_SHOW_SCHEMA", 0],
@@ -56720,8 +57368,9 @@ var PRIVATE_DATA$4 = {
56720
57368
  _selected_paint_ids: [],
56721
57369
  _pending_focus_topic: null,
56722
57370
  _pending_focus_all: false,
56723
- _find_hidden_matches: 0,
56724
- _pending_find: null,
57371
+ _find_term: "",
57372
+ _find_current: "",
57373
+ _find_found: null,
56725
57374
  _layout_asked: "",
56726
57375
  _nodes_total: 0,
56727
57376
  _nodes_placed: 0,
@@ -58676,6 +59325,14 @@ function graph_focus_topic(gobj, topic, reveal_all) {
58676
59325
  }
58677
59326
  let model = priv._fold_model;
58678
59327
  let state = priv._fold_state;
59328
+ if ((0, _yuneta_gobj_js.empty_string)(topic) && priv._find_term) {
59329
+ priv._focus_topic = null;
59330
+ return;
59331
+ }
59332
+ if (!(0, _yuneta_gobj_js.empty_string)(topic)) {
59333
+ priv._find_term = "";
59334
+ priv._find_current = "";
59335
+ }
58679
59336
  let keys = [];
58680
59337
  if (!(0, _yuneta_gobj_js.empty_string)(topic)) {
58681
59338
  for (let node of model.nodes.values()) if (node.topic_name === topic) keys.push(node.key);
@@ -58691,17 +59348,17 @@ function graph_focus_topic(gobj, topic, reveal_all) {
58691
59348
  }
58692
59349
  priv._focus_topic = topic || null;
58693
59350
  reconcile_fold(gobj, {}).then(() => {
58694
- apply_focus_ids(gobj, keys, "focus topic");
59351
+ apply_focus_ids(gobj, keys, "focus topic", true);
58695
59352
  });
58696
59353
  }
58697
59354
  /************************************************************
58698
- * Paint the highlight on these records (by model key) and put
58699
- * the first of them in the middle. Shared by the find and the
58700
- * topic focus, after the reconcile that brought them on screen.
58701
- * Keys that are still off screen are skipped: a match beyond the
58702
- * page a reveal opened is counted, not painted.
59355
+ * Paint the highlight on these records (by model key) and, with
59356
+ * `center`, put the first of them in the middle. Shared by the
59357
+ * find and the topic focus. Keys that are not on screen are
59358
+ * skipped: a match beyond the page a reveal opened is counted,
59359
+ * not painted.
58703
59360
  ************************************************************/
58704
- function apply_focus_ids(gobj, keys, what) {
59361
+ function apply_focus_ids(gobj, keys, what, center) {
58705
59362
  let priv = gobj.priv;
58706
59363
  let graph = priv.graph;
58707
59364
  if (!graph) return;
@@ -58726,7 +59383,7 @@ function apply_focus_ids(gobj, keys, what) {
58726
59383
  try {
58727
59384
  if (typeof graph.setElementState === "function") graph.setElementState(states);
58728
59385
  apply_node_highlight(gobj, prev_ids, ids);
58729
- if (ids.length) yui_graph_center_on(graph, ids[0]);
59386
+ if (center && ids.length) yui_graph_center_on(graph, ids[0]);
58730
59387
  } catch (e) {
58731
59388
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${what} apply failed: ${e}`);
58732
59389
  }
@@ -58737,61 +59394,139 @@ function node_id_of_key(gobj, key) {
58737
59394
  return build_node_name(gobj, key.slice(0, i), key.slice(i + 1));
58738
59395
  }
58739
59396
  /************************************************************
58740
- * Find the nodes whose name, id or topic contains `term`, highlight
58741
- * them with the same amber 'active' state the topic focus uses, and
58742
- * centre the viewport on them. Returns how many matched, which is the
58743
- * half the toolbar needs: a search that finds nothing must SAY so,
58744
- * because a graph that did not move is also what a graph looks like
58745
- * when the match is off-screen.
59397
+ * The find box: highlight the cards ON SCREEN whose name, id or
59398
+ * topic contains `term`, and change nothing else -- no group is
59399
+ * opened, no layout runs, the camera does not move. The graph the
59400
+ * reader arranged is the graph they are searching; emptying the
59401
+ * box takes the amber off and leaves it exactly as it was.
59402
+ *
59403
+ * It used to search the whole treedb and unfold a page of what it
59404
+ * found, which re-laid the graph out on every keystroke and left
59405
+ * the unfolded groups behind when the box was cleared. Opening up
59406
+ * to a topic is the legend's focus (graph_focus_topic); this only
59407
+ * LOOKS.
59408
+ *
59409
+ * The term is kept and repainted after every reconcile (see
59410
+ * reconcile_fold_now): a group opened by hand shows its matches
59411
+ * already lit, and the count follows what is on screen.
58746
59412
  *
58747
59413
  * It shares the highlight slot with graph_focus_topic on purpose —
58748
- * two amber sets at once would say nothing about either. An empty
58749
- * term clears it. Matching reads the node's LABEL and not only its
58750
- * id: on a topic keyed by rowid or by a qualified path, the id is a
58751
- * counter and the name a human knows is elsewhere (see node_label).
59414
+ * two amber sets at once would say nothing about either. Matching
59415
+ * reads the node's LABEL and not only its id: on a topic keyed by
59416
+ * rowid or by a qualified path, the id is a counter and the name a
59417
+ * human knows is elsewhere (see node_label).
58752
59418
  ************************************************************/
58753
59419
  function graph_find_nodes(gobj, term) {
58754
59420
  let priv = gobj.priv;
58755
- if (!priv.graph || !priv.graph_rendered || !priv._fold_model) {
58756
- priv._pending_find = term;
58757
- return 0;
59421
+ let next = (0, _yuneta_gobj_js.empty_string)(term) ? "" : String(term);
59422
+ let was = priv._find_term;
59423
+ priv._find_term = next;
59424
+ priv._find_current = "";
59425
+ priv._find_found = null;
59426
+ let ready = priv.graph && priv.graph_rendered && priv._fold_model;
59427
+ if (!next) {
59428
+ if (was && ready && !priv._focus_topic) apply_focus_ids(gobj, [], "find", false);
59429
+ publish_find_result(gobj);
59430
+ return;
58758
59431
  }
59432
+ priv._focus_topic = null;
59433
+ if (!ready) return;
59434
+ refresh_find(gobj, fold_visible_set(priv._fold_model, priv._fold_state));
59435
+ }
59436
+ /************************************************************
59437
+ * Repaint the find's highlight over the `visible` set and publish
59438
+ * the count. A no-op while no term is live, so the topic focus
59439
+ * keeps the slot.
59440
+ ************************************************************/
59441
+ function refresh_find(gobj, visible) {
59442
+ let priv = gobj.priv;
59443
+ if (!priv._find_term) return;
59444
+ let found = find_matches(gobj, priv._find_term, visible);
59445
+ apply_focus_ids(gobj, found.keys, "find", false);
59446
+ if (priv._find_current && (priv._focus_ids || []).indexOf(priv._find_current) < 0) priv._find_current = "";
59447
+ priv._find_found = found;
59448
+ publish_find_result(gobj);
59449
+ }
59450
+ /************************************************************
59451
+ * The records matching `term`: the keys ON SCREEN, and how many
59452
+ * more match that are NOT -- folded away in the fold model, or in
59453
+ * a hidden topic. Those two are only counted: "0" alone would read
59454
+ * as "does not exist" when it means "is not on screen".
59455
+ ************************************************************/
59456
+ function find_matches(gobj, term, visible) {
59457
+ let priv = gobj.priv;
58759
59458
  let model = priv._fold_model;
58760
- let state = priv._fold_state;
58761
- let keys = [];
58762
- let hidden_matches = 0;
58763
- if (!(0, _yuneta_gobj_js.empty_string)(term)) {
58764
- let needle = String(term).toLowerCase();
58765
- let matches = (topic_name, record) => {
58766
- let label = "";
58767
- try {
58768
- label = node_label(priv.descs[topic_name] || {}, record) || "";
58769
- } catch (e) {
58770
- label = "";
58771
- }
58772
- return [
58773
- label,
58774
- record.id,
58775
- topic_name
58776
- ].filter((v) => typeof v === "string").join(" ").toLowerCase().includes(needle);
58777
- };
58778
- for (let node of model.nodes.values()) if (matches(node.topic_name, node.record || {})) keys.push(node.key);
58779
- for (let topic_name of model.hidden) for (let record of priv.records[topic_name] || []) if ((0, _yuneta_gobj_js.is_object)(record) && matches(topic_name, record)) hidden_matches++;
59459
+ let found = {
59460
+ keys: [],
59461
+ folded: 0,
59462
+ hidden: 0
59463
+ };
59464
+ if (!model || (0, _yuneta_gobj_js.empty_string)(term)) return found;
59465
+ let needle = String(term).toLowerCase();
59466
+ let matches = (topic_name, record) => {
59467
+ let label = "";
59468
+ try {
59469
+ label = node_label(priv.descs[topic_name] || {}, record) || "";
59470
+ } catch (e) {
59471
+ label = "";
59472
+ }
59473
+ return [
59474
+ label,
59475
+ record.id,
59476
+ topic_name
59477
+ ].filter((v) => typeof v === "string").join(" ").toLowerCase().includes(needle);
59478
+ };
59479
+ for (let node of model.nodes.values()) if (matches(node.topic_name, node.record || {})) {
59480
+ if (visible.has(node.key)) found.keys.push(node.key);
59481
+ else found.folded++;
58780
59482
  }
58781
- priv._find_hidden_matches = hidden_matches;
58782
- let visible = fold_visible_set(model, state);
58783
- let revealed = 0;
58784
- for (let key of keys) {
58785
- if (visible.has(key)) continue;
58786
- if (revealed >= state.page_size) break;
58787
- fold_reveal(model, state, key);
58788
- revealed++;
59483
+ for (let topic_name of model.hidden) for (let record of priv.records[topic_name] || []) if ((0, _yuneta_gobj_js.is_object)(record) && matches(topic_name, record)) found.hidden++;
59484
+ return found;
59485
+ }
59486
+ /************************************************************
59487
+ * The lit cards in reading order: top to bottom, then left to
59488
+ * right. The order Enter walks, and the one the "k/N" counts in.
59489
+ ************************************************************/
59490
+ function find_order(gobj) {
59491
+ let priv = gobj.priv;
59492
+ let graph = priv.graph;
59493
+ let ids = (priv._focus_ids || []).slice();
59494
+ if (!graph) return ids;
59495
+ let pos = {};
59496
+ for (let id of ids) {
59497
+ let p = null;
59498
+ try {
59499
+ p = graph.getElementPosition(id);
59500
+ } catch (e) {
59501
+ p = null;
59502
+ }
59503
+ pos[id] = p || [0, 0];
58789
59504
  }
58790
- priv._focus_topic = null;
58791
- reconcile_fold(gobj, {}).then(() => {
58792
- apply_focus_ids(gobj, keys, "find");
58793
- });
58794
- return keys.length;
59505
+ ids.sort((a, b) => pos[a][1] - pos[b][1] || pos[a][0] - pos[b][0]);
59506
+ return ids;
59507
+ }
59508
+ /************************************************************
59509
+ * Enter in the find box: centre the next lit card (`back`: the
59510
+ * previous one). The one camera move of the find, and the reader
59511
+ * asks for it: the highlight alone cannot take anybody to a match
59512
+ * that is outside the frame.
59513
+ ************************************************************/
59514
+ function graph_find_step(gobj, back) {
59515
+ let priv = gobj.priv;
59516
+ let graph = priv.graph;
59517
+ if (!graph || !priv._find_term) return;
59518
+ let ids = find_order(gobj);
59519
+ if (ids.length === 0) return;
59520
+ let i = ids.indexOf(priv._find_current);
59521
+ if (i < 0) i = back ? ids.length - 1 : 0;
59522
+ else i = (i + (back ? ids.length - 1 : 1)) % ids.length;
59523
+ priv._find_current = ids[i];
59524
+ try {
59525
+ yui_graph_center_on(graph, ids[i]);
59526
+ } catch (e) {
59527
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: find step failed: ${e}`);
59528
+ }
59529
+ publish_find_result(gobj);
58795
59530
  }
58796
59531
  async function graph_zoom_in(gobj) {
58797
59532
  let graph = gobj.priv.graph;
@@ -62683,6 +63418,7 @@ async function reconcile_fold_now(gobj, opts) {
62683
63418
  history_resume(gobj);
62684
63419
  }
62685
63420
  publish_legend_state(gobj, visible);
63421
+ refresh_find(gobj, visible);
62686
63422
  return changed;
62687
63423
  }
62688
63424
  /************************************************************
@@ -63064,11 +63800,6 @@ function ac_load_data(gobj, event, kw, src) {
63064
63800
  priv._pending_focus_all = false;
63065
63801
  graph_focus_topic(gobj, ft, all);
63066
63802
  }
63067
- if (priv._pending_find !== null) {
63068
- let term = priv._pending_find;
63069
- priv._pending_find = null;
63070
- publish_find_result(gobj, term, graph_find_nodes(gobj, term));
63071
- }
63072
63803
  });
63073
63804
  }
63074
63805
  return 0;
@@ -63637,23 +64368,45 @@ function ac_node_dblclick(gobj, event, kw, src) {
63637
64368
  return 0;
63638
64369
  }
63639
64370
  /************************************************************
63640
- * Tell whoever asked how many nodes the term matched. The count is
63641
- * the answer to the question the box asks; without it "nothing moved"
63642
- * and "nothing matched" look the same.
64371
+ * Tell whoever asked how many cards the term lit. The count is the
64372
+ * answer to the question the box asks; without it "nothing lit" and
64373
+ * "nothing matched" look the same.
64374
+ *
64375
+ * matches lit cards (on screen)
64376
+ * folded_matches matches folded away, not on screen
64377
+ * hidden_matches matches in hidden topics
64378
+ * current 1-based place of the card Enter centred on
64379
+ * among the lit ones (reading order), 0 = none
63643
64380
  ************************************************************/
63644
- function publish_find_result(gobj, term, matches) {
64381
+ function publish_find_result(gobj) {
64382
+ let priv = gobj.priv;
64383
+ let found = priv._find_found || {
64384
+ keys: [],
64385
+ folded: 0,
64386
+ hidden: 0
64387
+ };
64388
+ let current = 0;
64389
+ if (priv._find_term && priv._find_current) current = find_order(gobj).indexOf(priv._find_current) + 1;
63645
64390
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_FIND_RESULT", {
63646
- term,
63647
- matches,
63648
- hidden_matches: gobj.priv._find_hidden_matches || 0
64391
+ term: priv._find_term,
64392
+ matches: priv._find_term ? (priv._focus_ids || []).length : 0,
64393
+ folded_matches: found.folded,
64394
+ hidden_matches: found.hidden,
64395
+ current
63649
64396
  });
63650
64397
  }
63651
64398
  /************************************************************
63652
64399
  *
63653
64400
  ************************************************************/
63654
64401
  function ac_find_nodes$1(gobj, event, kw, src) {
63655
- let term = kw && kw.text || "";
63656
- publish_find_result(gobj, term, graph_find_nodes(gobj, term));
64402
+ graph_find_nodes(gobj, kw && kw.text || "");
64403
+ return 0;
64404
+ }
64405
+ /************************************************************
64406
+ * Enter in the find box: {back: true} for Shift+Enter.
64407
+ ************************************************************/
64408
+ function ac_find_next$1(gobj, event, kw, src) {
64409
+ graph_find_step(gobj, !!(kw && kw.back));
63657
64410
  return 0;
63658
64411
  }
63659
64412
  function ac_center(gobj, event, kw, src) {
@@ -64114,6 +64867,11 @@ function create_gclass$4(gclass_name) {
64114
64867
  ac_find_nodes$1,
64115
64868
  null
64116
64869
  ],
64870
+ [
64871
+ "EV_FIND_NEXT",
64872
+ ac_find_next$1,
64873
+ null
64874
+ ],
64117
64875
  [
64118
64876
  "EV_TOGGLE_FOLD",
64119
64877
  ac_toggle_fold,
@@ -64269,6 +65027,7 @@ function create_gclass$4(gclass_name) {
64269
65027
  ["EV_ZOOM_SELECTION", 0],
64270
65028
  ["EV_FOCUS_TOPIC", 0],
64271
65029
  ["EV_FIND_NODES", 0],
65030
+ ["EV_FIND_NEXT", 0],
64272
65031
  ["EV_FIND_RESULT", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
64273
65032
  ["EV_LAYOUT_AUTOSET", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
64274
65033
  ["EV_CAMERA_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
@@ -65061,19 +65820,34 @@ function make_toolbar(gobj) {
65061
65820
  type: "text",
65062
65821
  placeholder: (0, i18next.t)("search"),
65063
65822
  "data-i18n-placeholder": "search",
65064
- "aria-label": (0, i18next.t)("search"),
65065
- "data-i18n-aria-label": "search"
65823
+ title: (0, i18next.t)("find on screen"),
65824
+ "data-i18n-title": "find on screen",
65825
+ "aria-label": (0, i18next.t)("find on screen"),
65826
+ "data-i18n-aria-label": "find on screen"
65066
65827
  },
65067
65828
  [],
65068
- { input: (evt) => {
65069
- evt.stopPropagation();
65070
- let text = evt.target.value.trim();
65071
- if (priv.find_timer) clearTimeout(priv.find_timer);
65072
- priv.find_timer = setTimeout(function() {
65073
- priv.find_timer = null;
65074
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FIND_NODES", { text }, gobj);
65075
- }, 250);
65076
- } }
65829
+ {
65830
+ input: (evt) => {
65831
+ evt.stopPropagation();
65832
+ let text = evt.target.value.trim();
65833
+ if (priv.find_timer) clearTimeout(priv.find_timer);
65834
+ priv.find_timer = setTimeout(function() {
65835
+ priv.find_timer = null;
65836
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FIND_NODES", { text }, gobj);
65837
+ }, 250);
65838
+ },
65839
+ keydown: (evt) => {
65840
+ if (evt.key !== "Enter" || evt.isComposing) return;
65841
+ evt.preventDefault();
65842
+ evt.stopPropagation();
65843
+ if (priv.find_timer) {
65844
+ clearTimeout(priv.find_timer);
65845
+ priv.find_timer = null;
65846
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FIND_NODES", { text: evt.target.value.trim() }, gobj);
65847
+ }
65848
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FIND_NEXT", { back: evt.shiftKey }, gobj);
65849
+ }
65850
+ }
65077
65851
  ]);
65078
65852
  let $find_control = (0, _yuneta_gobj_js.createElement2)([
65079
65853
  "div",
@@ -66402,6 +67176,14 @@ function ac_find_nodes(gobj, event, kw, src) {
66402
67176
  return 0;
66403
67177
  }
66404
67178
  /************************************************************
67179
+ * Enter in the find box: forward the step down to the graph child.
67180
+ ************************************************************/
67181
+ function ac_find_next(gobj, event, kw, src) {
67182
+ let priv = gobj.priv;
67183
+ if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_FIND_NEXT", { back: !!(kw && kw.back) }, gobj);
67184
+ return 0;
67185
+ }
67186
+ /************************************************************
66405
67187
  * One press of the fold stepper (EV_EXPAND_LEVEL /
66406
67188
  * EV_COLLAPSE_LEVEL): the level moves by one, is SAVED (the
66407
67189
  * reader's, per treedb) and reaches the G6 child, which owns
@@ -66485,12 +67267,33 @@ function refresh_find_result(gobj) {
66485
67267
  return 0;
66486
67268
  }
66487
67269
  let text = (0, i18next.t)("matches", { count: kw && kw.matches || 0 });
67270
+ let current = kw && kw.current || 0;
67271
+ if (current > 0) text = `${current}/${text}`;
67272
+ let extra = [];
67273
+ let folded = kw && kw.folded_matches || 0;
67274
+ if (folded > 0) extra.push(`+${folded} ${(0, i18next.t)("not shown")}`);
66488
67275
  let hidden = kw && kw.hidden_matches || 0;
66489
- if (hidden > 0) text += ` (+${hidden} ${(0, i18next.t)("hidden topics")})`;
67276
+ if (hidden > 0) extra.push(`+${hidden} ${(0, i18next.t)("hidden topics")}`);
67277
+ if (extra.length) text += ` (${extra.join(", ")})`;
66490
67278
  $count.textContent = text;
66491
67279
  $result.classList.remove("is-hidden");
66492
67280
  return 0;
66493
67281
  }
67282
+ function clear_find_box(gobj) {
67283
+ let priv = gobj.priv;
67284
+ if (priv.find_timer) {
67285
+ clearTimeout(priv.find_timer);
67286
+ priv.find_timer = null;
67287
+ }
67288
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
67289
+ let $input = $container ? $container.querySelector(".GRAPH_FIND_INPUT") : null;
67290
+ if ($input) {
67291
+ $input.value = "";
67292
+ refresh_clear($input);
67293
+ }
67294
+ priv._find_result = null;
67295
+ refresh_find_result(gobj);
67296
+ }
66494
67297
  function ac_set_focus_topic(gobj, event, kw, src) {
66495
67298
  let priv = gobj.priv;
66496
67299
  priv.focus_topic = kw && kw.topic || "";
@@ -66501,6 +67304,7 @@ function ac_set_focus_topic(gobj, event, kw, src) {
66501
67304
  set_legend_list_attr(gobj, "hidden_topics", "EV_SET_HIDDEN_TOPICS", hidden);
66502
67305
  }
66503
67306
  }
67307
+ if (priv.focus_topic) clear_find_box(gobj);
66504
67308
  if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_FOCUS_TOPIC", {
66505
67309
  topic: priv.focus_topic,
66506
67310
  reveal: kw && kw.reveal || "page"
@@ -66678,6 +67482,11 @@ function create_gclass$3(gclass_name) {
66678
67482
  ac_find_nodes,
66679
67483
  null
66680
67484
  ],
67485
+ [
67486
+ "EV_FIND_NEXT",
67487
+ ac_find_next,
67488
+ null
67489
+ ],
66681
67490
  [
66682
67491
  "EV_EXPAND_LEVEL",
66683
67492
  ac_fold_level_step,
@@ -66776,6 +67585,7 @@ function create_gclass$3(gclass_name) {
66776
67585
  ["EV_SET_OPERATION_MODE", 0],
66777
67586
  ["EV_SET_FOCUS_TOPIC", 0],
66778
67587
  ["EV_FIND_NODES", 0],
67588
+ ["EV_FIND_NEXT", 0],
66779
67589
  ["EV_EXPAND_LEVEL", 0],
66780
67590
  ["EV_COLLAPSE_LEVEL", 0],
66781
67591
  ["EV_SET_NODE_MODE", 0],