@yuneta/gobj-ui 7.23.158 → 7.23.160

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.
@@ -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],
@@ -11253,6 +11500,64 @@ function yui_graph_camera_behaviors() {
11253
11500
  }];
11254
11501
  }
11255
11502
  /************************************************************
11503
+ * Make the wheel work over HTML nodes too.
11504
+ *
11505
+ * G6's HTML nodes live in a DOM layer BESIDE the canvas
11506
+ * element, not in it, and they forward six pointer events and
11507
+ * not the wheel. `scroll-canvas` listens on the canvas element
11508
+ * and `zoom-canvas` reads G's own wheel event, which is fed
11509
+ * from that same element -- so a wheel over a card reaches
11510
+ * neither, and on a graph drawn with big cards the wheel only
11511
+ * works in the gaps between them.
11512
+ *
11513
+ * The wheel is caught on `$host` (the element G6 was mounted
11514
+ * in) and dispatched again ON the canvas element, carrying the
11515
+ * deltas, the position and the modifiers: both behaviors then
11516
+ * take it exactly as a wheel over empty canvas. Control is
11517
+ * read by G6 from the real keydown, which is still held.
11518
+ *
11519
+ * A wheel whose target is the canvas passes untouched, which
11520
+ * is also what keeps the re-dispatched one from looping. An
11521
+ * element that must scroll by itself (a popover) stops the
11522
+ * wheel before it gets here.
11523
+ *
11524
+ * Returns the function that removes the listener.
11525
+ ************************************************************/
11526
+ function yui_graph_forward_wheel(graph, $host) {
11527
+ if (!graph || !$host) return () => {};
11528
+ const on_wheel = (e) => {
11529
+ let dom = null;
11530
+ try {
11531
+ dom = graph.getCanvas().getContextService().getDomElement();
11532
+ } catch (err) {
11533
+ return;
11534
+ }
11535
+ if (!dom || e.target === dom) return;
11536
+ e.preventDefault();
11537
+ e.stopPropagation();
11538
+ dom.dispatchEvent(new WheelEvent("wheel", {
11539
+ deltaX: e.deltaX,
11540
+ deltaY: e.deltaY,
11541
+ deltaZ: e.deltaZ,
11542
+ deltaMode: e.deltaMode,
11543
+ clientX: e.clientX,
11544
+ clientY: e.clientY,
11545
+ screenX: e.screenX,
11546
+ screenY: e.screenY,
11547
+ ctrlKey: e.ctrlKey,
11548
+ shiftKey: e.shiftKey,
11549
+ altKey: e.altKey,
11550
+ metaKey: e.metaKey,
11551
+ bubbles: true,
11552
+ cancelable: true
11553
+ }));
11554
+ };
11555
+ $host.addEventListener("wheel", on_wheel, { passive: false });
11556
+ return () => {
11557
+ $host.removeEventListener("wheel", on_wheel);
11558
+ };
11559
+ }
11560
+ /************************************************************
11256
11561
  * The camera cluster: zoom in, zoom out, readout, fit,
11257
11562
  * actual size. Returns createElement2 specs, in order.
11258
11563
  *
@@ -13314,7 +13619,7 @@ function ac_toggle_anchor$2(gobj, event, kw, src) {
13314
13619
  /************************************************************
13315
13620
  *
13316
13621
  ************************************************************/
13317
- function ac_zoom_in$3(gobj, event, kw, src) {
13622
+ function ac_zoom_in$4(gobj, event, kw, src) {
13318
13623
  let graph = gobj.priv.graph;
13319
13624
  if (graph) {
13320
13625
  let z = graph.getZoom();
@@ -13325,7 +13630,7 @@ function ac_zoom_in$3(gobj, event, kw, src) {
13325
13630
  /************************************************************
13326
13631
  *
13327
13632
  ************************************************************/
13328
- function ac_zoom_out$3(gobj, event, kw, src) {
13633
+ function ac_zoom_out$4(gobj, event, kw, src) {
13329
13634
  let graph = gobj.priv.graph;
13330
13635
  if (graph) {
13331
13636
  let z = graph.getZoom();
@@ -13336,7 +13641,7 @@ function ac_zoom_out$3(gobj, event, kw, src) {
13336
13641
  /************************************************************
13337
13642
  *
13338
13643
  ************************************************************/
13339
- function ac_zoom_reset$3(gobj, event, kw, src) {
13644
+ function ac_zoom_reset$4(gobj, event, kw, src) {
13340
13645
  let priv = gobj.priv;
13341
13646
  let graph = priv.graph;
13342
13647
  if (graph && graph.rendered) {
@@ -13351,7 +13656,7 @@ function ac_zoom_reset$3(gobj, event, kw, src) {
13351
13656
  /************************************************************
13352
13657
  *
13353
13658
  ************************************************************/
13354
- function ac_center$3(gobj, event, kw, src) {
13659
+ function ac_center$4(gobj, event, kw, src) {
13355
13660
  let graph = gobj.priv.graph;
13356
13661
  if (graph) {
13357
13662
  graph.fitCenter();
@@ -13487,22 +13792,22 @@ function create_gclass$14(gclass_name) {
13487
13792
  ],
13488
13793
  [
13489
13794
  "EV_ZOOM_IN",
13490
- ac_zoom_in$3,
13795
+ ac_zoom_in$4,
13491
13796
  null
13492
13797
  ],
13493
13798
  [
13494
13799
  "EV_ZOOM_OUT",
13495
- ac_zoom_out$3,
13800
+ ac_zoom_out$4,
13496
13801
  null
13497
13802
  ],
13498
13803
  [
13499
13804
  "EV_ZOOM_RESET",
13500
- ac_zoom_reset$3,
13805
+ ac_zoom_reset$4,
13501
13806
  null
13502
13807
  ],
13503
13808
  [
13504
13809
  "EV_CENTER",
13505
- ac_center$3,
13810
+ ac_center$4,
13506
13811
  null
13507
13812
  ],
13508
13813
  [
@@ -16599,16 +16904,16 @@ function ac_language_changed$7(gobj, event, kw, src) {
16599
16904
  /************************************************************
16600
16905
  * The camera.
16601
16906
  ************************************************************/
16602
- function ac_zoom_in$2(gobj, event, kw, src) {
16907
+ function ac_zoom_in$3(gobj, event, kw, src) {
16603
16908
  return camera(gobj, "zoom in", (graph) => graph.zoomBy(1.2));
16604
16909
  }
16605
- function ac_zoom_out$2(gobj, event, kw, src) {
16910
+ function ac_zoom_out$3(gobj, event, kw, src) {
16606
16911
  return camera(gobj, "zoom out", (graph) => graph.zoomBy(1 / 1.2));
16607
16912
  }
16608
- function ac_zoom_reset$2(gobj, event, kw, src) {
16913
+ function ac_zoom_reset$3(gobj, event, kw, src) {
16609
16914
  return camera(gobj, "reset the zoom", (graph) => graph.zoomTo(1));
16610
16915
  }
16611
- function ac_center$2(gobj, event, kw, src) {
16916
+ function ac_center$3(gobj, event, kw, src) {
16612
16917
  return camera(gobj, "fit the graph", (graph) => fit_readable(graph));
16613
16918
  }
16614
16919
  /************************************************************
@@ -16667,22 +16972,22 @@ function create_gclass$11(gclass_name) {
16667
16972
  ],
16668
16973
  [
16669
16974
  "EV_ZOOM_IN",
16670
- ac_zoom_in$2,
16975
+ ac_zoom_in$3,
16671
16976
  null
16672
16977
  ],
16673
16978
  [
16674
16979
  "EV_ZOOM_OUT",
16675
- ac_zoom_out$2,
16980
+ ac_zoom_out$3,
16676
16981
  null
16677
16982
  ],
16678
16983
  [
16679
16984
  "EV_ZOOM_RESET",
16680
- ac_zoom_reset$2,
16985
+ ac_zoom_reset$3,
16681
16986
  null
16682
16987
  ],
16683
16988
  [
16684
16989
  "EV_CENTER",
16685
- ac_center$2,
16990
+ ac_center$3,
16686
16991
  null
16687
16992
  ],
16688
16993
  [
@@ -20790,7 +21095,7 @@ function ac_toggle_anchor$1(gobj, event, kw, src) {
20790
21095
  /************************************************************
20791
21096
  *
20792
21097
  ************************************************************/
20793
- function ac_zoom_in$1(gobj, event, kw, src) {
21098
+ function ac_zoom_in$2(gobj, event, kw, src) {
20794
21099
  let graph = gobj.priv.graph;
20795
21100
  if (graph) {
20796
21101
  let z = graph.getZoom();
@@ -20801,7 +21106,7 @@ function ac_zoom_in$1(gobj, event, kw, src) {
20801
21106
  /************************************************************
20802
21107
  *
20803
21108
  ************************************************************/
20804
- function ac_zoom_out$1(gobj, event, kw, src) {
21109
+ function ac_zoom_out$2(gobj, event, kw, src) {
20805
21110
  let graph = gobj.priv.graph;
20806
21111
  if (graph) {
20807
21112
  let z = graph.getZoom();
@@ -20812,7 +21117,7 @@ function ac_zoom_out$1(gobj, event, kw, src) {
20812
21117
  /************************************************************
20813
21118
  *
20814
21119
  ************************************************************/
20815
- function ac_zoom_reset$1(gobj, event, kw, src) {
21120
+ function ac_zoom_reset$2(gobj, event, kw, src) {
20816
21121
  let priv = gobj.priv;
20817
21122
  let graph = priv.graph;
20818
21123
  if (graph && graph.rendered) {
@@ -20827,7 +21132,7 @@ function ac_zoom_reset$1(gobj, event, kw, src) {
20827
21132
  /************************************************************
20828
21133
  *
20829
21134
  ************************************************************/
20830
- function ac_center$1(gobj, event, kw, src) {
21135
+ function ac_center$2(gobj, event, kw, src) {
20831
21136
  let graph = gobj.priv.graph;
20832
21137
  if (graph) {
20833
21138
  graph.fitCenter();
@@ -21033,22 +21338,22 @@ function create_gclass$9(gclass_name) {
21033
21338
  ],
21034
21339
  [
21035
21340
  "EV_ZOOM_IN",
21036
- ac_zoom_in$1,
21341
+ ac_zoom_in$2,
21037
21342
  null
21038
21343
  ],
21039
21344
  [
21040
21345
  "EV_ZOOM_OUT",
21041
- ac_zoom_out$1,
21346
+ ac_zoom_out$2,
21042
21347
  null
21043
21348
  ],
21044
21349
  [
21045
21350
  "EV_ZOOM_RESET",
21046
- ac_zoom_reset$1,
21351
+ ac_zoom_reset$2,
21047
21352
  null
21048
21353
  ],
21049
21354
  [
21050
21355
  "EV_CENTER",
21051
- ac_center$1,
21356
+ ac_center$2,
21052
21357
  null
21053
21358
  ],
21054
21359
  [
@@ -22754,6 +23059,7 @@ var attrs_table$6 = [
22754
23059
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
22755
23060
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
22756
23061
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
23062
+ 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
23063
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
22758
23064
  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
23065
  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 +24379,24 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
24073
24379
  else gobj_send_event(jv, "EV_SET_JSON", { json: data }, gobj);
24074
24380
  return 0;
24075
24381
  }
24382
+ if (command === "get-asset") {
24383
+ let asset_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
24384
+ let req_id = kw_get_str(gobj, kw_command, "req_id", "", 0);
24385
+ let gobj_topic_form = gobj_find_child(gobj, { __gobj_name__: `${gobj_name(gobj)}?${asset_topic}` });
24386
+ if (!gobj_topic_form || !req_id) {
24387
+ log_error(`${gobj_short_name(gobj)}: get-asset answer for nobody: topic '${asset_topic}', req_id '${req_id}'`);
24388
+ return 0;
24389
+ }
24390
+ if (result < 0) gobj_send_event(gobj_topic_form, "EV_ASSET_FAILED", {
24391
+ req_id,
24392
+ error: comment || "get-asset failed"
24393
+ }, gobj);
24394
+ else gobj_send_event(gobj_topic_form, "EV_ASSET_LOADED", {
24395
+ req_id,
24396
+ answer: data
24397
+ }, gobj);
24398
+ return 0;
24399
+ }
24076
24400
  if (result < 0) {
24077
24401
  if (command === "descs") show_load_error$1(gobj, t(comment));
24078
24402
  else {
@@ -24354,6 +24678,48 @@ function ac_request_page(gobj, event, kw, src) {
24354
24678
  return 0;
24355
24679
  }
24356
24680
  /********************************************
24681
+ * Event from formtable: the bytes of one asset a `file` column names,
24682
+ * for the form's preview or the table's file popup.
24683
+ *
24684
+ * Asked to the ASSETS service and not to the treedb: storing is the
24685
+ * treedb's, the way out is C_ASSETS's (a signed url when a web server
24686
+ * sits in front of the store, the bytes inline when there is none). The
24687
+ * answer goes back down to the table that asked, by `req_id`.
24688
+ *
24689
+ * A read: a read-only treedb still answers it.
24690
+ ********************************************/
24691
+ function ac_request_asset(gobj, event, kw, src) {
24692
+ if (!kw.req_id || !kw.asset_id) {
24693
+ log_error(`${gobj_short_name(gobj)}: an asset request with no id`);
24694
+ return -1;
24695
+ }
24696
+ let assets_service = gobj_read_str_attr(gobj, "assets_service");
24697
+ let gobj_remote_yuno = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
24698
+ if (!assets_service || !gobj_remote_yuno) {
24699
+ gobj_send_event(src, "EV_ASSET_FAILED", {
24700
+ req_id: kw.req_id,
24701
+ error: assets_service ? "cannot reach the backend" : "no assets service"
24702
+ }, gobj);
24703
+ return 0;
24704
+ }
24705
+ let ret = gobj_command(gobj_remote_yuno, "get-asset", {
24706
+ service: assets_service,
24707
+ asset_id: kw.asset_id,
24708
+ __md_command__: {
24709
+ topic_name: kw.topic_name || gobj_read_attr(src, "topic_name"),
24710
+ req_id: kw.req_id
24711
+ }
24712
+ }, gobj);
24713
+ if (ret) {
24714
+ log_error(ret);
24715
+ gobj_send_event(src, "EV_ASSET_FAILED", {
24716
+ req_id: kw.req_id,
24717
+ error: "cannot reach the backend"
24718
+ }, gobj);
24719
+ }
24720
+ return 0;
24721
+ }
24722
+ /********************************************
24357
24723
  * Event from formtable: ONE field of one record, edited in place.
24358
24724
  *
24359
24725
  * Written as a PARTIAL update and, deliberately, with NO `autolink`.
@@ -24539,6 +24905,11 @@ function create_gclass$6(gclass_name) {
24539
24905
  ac_request_page,
24540
24906
  null
24541
24907
  ],
24908
+ [
24909
+ "EV_REQUEST_ASSET",
24910
+ ac_request_asset,
24911
+ null
24912
+ ],
24542
24913
  [
24543
24914
  "EV_DELETE_RECORD",
24544
24915
  ac_delete_record,
@@ -24619,6 +24990,7 @@ function create_gclass$6(gclass_name) {
24619
24990
  ["EV_UPDATE_RECORD", 0],
24620
24991
  ["EV_UPDATE_FIELD", 0],
24621
24992
  ["EV_REQUEST_PAGE", 0],
24993
+ ["EV_REQUEST_ASSET", 0],
24622
24994
  ["EV_DELETE_RECORD", 0],
24623
24995
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
24624
24996
  ["EV_REFRESH_TOPIC", 0],
@@ -25424,7 +25796,12 @@ var PRIVATE_DATA$5 = {
25424
25796
  schema_win: null,
25425
25797
  cell_json_gobj: null,
25426
25798
  cell_json_modal: null,
25427
- cell_json_win: null
25799
+ cell_json_win: null,
25800
+ cell_file_box: null,
25801
+ cell_file_modal: null,
25802
+ cell_file_win: null,
25803
+ _pending_assets: null,
25804
+ _asset_seq: 0
25428
25805
  };
25429
25806
  var __gclass__$5 = null;
25430
25807
  function is_mobile$1() {
@@ -25557,6 +25934,8 @@ function mt_stop$5(gobj) {
25557
25934
  close_form_dialog(gobj);
25558
25935
  close_schema_dialog(gobj);
25559
25936
  close_cell_json_dialog(gobj);
25937
+ close_cell_file_dialog(gobj);
25938
+ drop_pending_assets(gobj);
25560
25939
  table__destroy(gobj);
25561
25940
  }
25562
25941
  /***************************************************************
@@ -25566,6 +25945,8 @@ function mt_destroy$5(gobj) {
25566
25945
  close_form_dialog(gobj);
25567
25946
  close_schema_dialog(gobj);
25568
25947
  close_cell_json_dialog(gobj);
25948
+ close_cell_file_dialog(gobj);
25949
+ drop_pending_assets(gobj);
25569
25950
  destroy_ui$2(gobj);
25570
25951
  }
25571
25952
  /************************************************************
@@ -26210,6 +26591,20 @@ function create_tabulator(gobj) {
26210
26591
  break;
26211
26592
  case "file":
26212
26593
  colFormatter = file_cell_formatter;
26594
+ cellClick = function(e, cell) {
26595
+ if (!e.target.closest(".FILE_CELL_LINK")) return;
26596
+ e.stopPropagation();
26597
+ let pkey = desc.pkey || "id";
26598
+ let row_id = cell.getData()[pkey];
26599
+ if (row_id === void 0 || row_id === null) {
26600
+ log_error(`${gobj_short_name(gobj)}: row without pkey '${pkey}', cannot open the file of '${cell.getField()}'`);
26601
+ return;
26602
+ }
26603
+ gobj_send_event(gobj, "EV_SHOW_CELL_FILE", {
26604
+ row_id,
26605
+ col_id: cell.getField()
26606
+ }, gobj);
26607
+ };
26213
26608
  break;
26214
26609
  case "object":
26215
26610
  case "dict":
@@ -26480,13 +26875,21 @@ function table__destroy(gobj) {
26480
26875
  * What a `file` column shows in a table cell: the asset it names.
26481
26876
  *
26482
26877
  * A sha256 in full is 64 characters of noise in a column, so it is
26483
- * shortened and the whole of it is the tooltip. Empty is SAID, not left
26484
- * blank -- a blank cell and a cell whose asset failed to load look the
26485
- * same, and only one of them is normal.
26878
+ * shortened and the whole of it goes to the popup the click opens. Empty
26879
+ * is SAID, not left blank -- a blank cell and a cell whose asset failed
26880
+ * to load look the same, and only one of them is normal.
26881
+ *
26882
+ * The value arrives already turned into ids joined by ", "
26883
+ * (transform__treedb_value_2_table_value), and an array column names
26884
+ * several: the first is shown and the rest counted.
26486
26885
  ************************************************************/
26886
+ function file_cell_ids(value) {
26887
+ if (is_array(value)) return yui_asset_ids(value);
26888
+ return String(value || "").split(",").map((s) => s.trim()).filter((s) => !!s).flatMap((s) => yui_asset_ids(s));
26889
+ }
26487
26890
  function file_cell_formatter(cell, formatterParams, onRendered) {
26488
- const id = yui_asset_id(cell.getValue());
26489
- if (!id) return createElement2([
26891
+ const ids = file_cell_ids(cell.getValue());
26892
+ if (!ids.length) return createElement2([
26490
26893
  "span",
26491
26894
  {
26492
26895
  class: "FILE_CELL is-empty",
@@ -26494,13 +26897,26 @@ function file_cell_formatter(cell, formatterParams, onRendered) {
26494
26897
  },
26495
26898
  "no file"
26496
26899
  ]);
26497
- return createElement2([
26900
+ const children = [["span", { class: "FILE_CELL_ICON icon yi-eye" }], [
26498
26901
  "span",
26902
+ { class: "FILE_CELL_ID" },
26903
+ yui_file_id_label(ids[0])
26904
+ ]];
26905
+ if (ids.length > 1) children.push([
26906
+ "span",
26907
+ { class: "FILE_CELL_MORE" },
26908
+ `+${ids.length - 1}`
26909
+ ]);
26910
+ return createElement2([
26911
+ "a",
26499
26912
  {
26500
- class: "FILE_CELL",
26501
- title: id
26913
+ class: "FILE_CELL FILE_CELL_LINK",
26914
+ title: t("show file"),
26915
+ "data-i18n-title": "show file",
26916
+ "aria-label": t("show file"),
26917
+ "data-i18n-aria-label": "show file"
26502
26918
  },
26503
- yui_file_id_label(id)
26919
+ children
26504
26920
  ]);
26505
26921
  }
26506
26922
  function icon_formatter(cell) {
@@ -26764,6 +27180,7 @@ function open_form_dialog(gobj, mode, record) {
26764
27180
  });
26765
27181
  gobj_start(form);
26766
27182
  refresh_language($body, t);
27183
+ request_form_previews(gobj, record);
26767
27184
  let $with_focus = $body.querySelector(".with-focus:not([readonly])");
26768
27185
  if (!$with_focus) $with_focus = $body.querySelector("input.input:not([readonly]), textarea, select");
26769
27186
  if ($with_focus) $with_focus.focus();
@@ -27011,6 +27428,225 @@ function close_cell_json_dialog(gobj) {
27011
27428
  teardown_cell_json_child(gobj);
27012
27429
  }
27013
27430
  /************************************************************
27431
+ * Ask the host for the bytes of ONE asset.
27432
+ *
27433
+ * Same contract as the page requests: the transport belongs to the
27434
+ * HOST, so the request goes up as an event and the answer comes back
27435
+ * down as one, correlated by a `req_id` the host echoes. What is parked
27436
+ * here is WHERE the answer goes -- the form's preview of a column, or a
27437
+ * slot of the file popup -- and the watchdog, because a request whose
27438
+ * answer never lands would leave a "loading" on screen for ever.
27439
+ *
27440
+ * `target` is kept here and never travels: it holds a gobj and a DOM
27441
+ * node, and a kw is plain json.
27442
+ ************************************************************/
27443
+ var ASSET_TIMEOUT_MS = 3e4;
27444
+ function request_asset(gobj, asset_id, target) {
27445
+ let priv = gobj.priv;
27446
+ if (!priv._pending_assets) priv._pending_assets = {};
27447
+ let req_id = `a${++priv._asset_seq}`;
27448
+ let timer = window.setTimeout(function() {
27449
+ gobj_send_event(gobj, "EV_ASSET_FAILED", {
27450
+ req_id,
27451
+ error: "asset request timed out"
27452
+ }, gobj);
27453
+ }, ASSET_TIMEOUT_MS);
27454
+ priv._pending_assets[req_id] = Object.assign({
27455
+ id: asset_id,
27456
+ timer
27457
+ }, target);
27458
+ gobj_publish_event(gobj, "EV_REQUEST_ASSET", {
27459
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
27460
+ req_id,
27461
+ asset_id
27462
+ });
27463
+ }
27464
+ /************************************************************
27465
+ * Take a parked asset request out, however it ended. Null when
27466
+ * it was already settled (a late answer after a timeout).
27467
+ ************************************************************/
27468
+ function take_pending_asset(gobj, req_id) {
27469
+ let priv = gobj.priv;
27470
+ let pend = priv._pending_assets ? priv._pending_assets[req_id] : null;
27471
+ if (!pend) return null;
27472
+ delete priv._pending_assets[req_id];
27473
+ if (pend.timer) window.clearTimeout(pend.timer);
27474
+ return pend;
27475
+ }
27476
+ /************************************************************
27477
+ * Forget every parked asset request: the view is going away,
27478
+ * and a watchdog firing into a stopped gobj is an error for
27479
+ * nothing.
27480
+ ************************************************************/
27481
+ function drop_pending_assets(gobj) {
27482
+ let priv = gobj.priv;
27483
+ if (priv._pending_assets) {
27484
+ for (const pend of Object.values(priv._pending_assets)) if (pend.timer) window.clearTimeout(pend.timer);
27485
+ }
27486
+ priv._pending_assets = null;
27487
+ }
27488
+ /************************************************************
27489
+ * Ask for the asset of every `file` column the record names,
27490
+ * for the form's preview. One per column: the form shows the
27491
+ * asset the column keeps, which is the first it names.
27492
+ ************************************************************/
27493
+ function request_form_previews(gobj, record) {
27494
+ let priv = gobj.priv;
27495
+ if (!record || !priv.form) return;
27496
+ let desc = gobj_read_attr(gobj, "desc");
27497
+ for (let col of desc.cols) {
27498
+ if (!col.id || col.id.charAt(0) === "_") continue;
27499
+ const field_desc = treedb_get_field_desc(col);
27500
+ if (field_desc.type !== "file" || field_desc.is_hidden) continue;
27501
+ let ids = file_cell_ids(record[col.id]);
27502
+ if (!ids.length) continue;
27503
+ request_asset(gobj, ids[0], {
27504
+ target: "form",
27505
+ form: priv.form,
27506
+ col: col.id
27507
+ });
27508
+ }
27509
+ }
27510
+ /************************************************************
27511
+ * Show what a `file` cell names -- every asset, when the
27512
+ * column is an array -- in the standardized adaptive dialog.
27513
+ *
27514
+ * Unlike the JSON cell this is NOT offline: the row holds the
27515
+ * ids and the bytes are the backend's, so each slot opens as
27516
+ * "loading" and one `get-asset` per id fills it.
27517
+ ************************************************************/
27518
+ function open_cell_file_dialog(gobj, row_id, col_id) {
27519
+ close_cell_file_dialog(gobj);
27520
+ let priv = gobj.priv;
27521
+ let tabulator = gobj_read_attr(gobj, "tabulator");
27522
+ if (!tabulator) {
27523
+ log_error(`${gobj_short_name(gobj)}: no table, no file cell to show`);
27524
+ return;
27525
+ }
27526
+ let row = tabulator.getRow(row_id);
27527
+ if (!row) {
27528
+ log_error(`${gobj_short_name(gobj)}: row '${row_id}' not found in the table`);
27529
+ return;
27530
+ }
27531
+ let ids = file_cell_ids(row.getData()[col_id]);
27532
+ if (!ids.length) {
27533
+ log_error(`${gobj_short_name(gobj)}: row '${row_id}' col '${col_id}' names no file`);
27534
+ return;
27535
+ }
27536
+ let $items = ids.map((id) => createElement2([
27537
+ "div",
27538
+ { class: "CELL_FILE_ITEM" },
27539
+ [[
27540
+ "div",
27541
+ { class: "CELL_FILE_SLOT" },
27542
+ [[
27543
+ "span",
27544
+ {
27545
+ class: "CELL_FILE_LOADING",
27546
+ i18n: "loading"
27547
+ },
27548
+ "loading"
27549
+ ]]
27550
+ ], [
27551
+ "div",
27552
+ { class: "CELL_FILE_CAPTION" },
27553
+ [[
27554
+ "span",
27555
+ { class: "CELL_FILE_ID" },
27556
+ id
27557
+ ]]
27558
+ ]]
27559
+ ]));
27560
+ let $box = createElement2([
27561
+ "div",
27562
+ { class: "TREEDB_CELL_FILE" },
27563
+ $items
27564
+ ]);
27565
+ let presented = present_json_popup(gobj, $box, {
27566
+ name: "filewin_" + clean_name(gobj_name(gobj)),
27567
+ logical_class: "TREEDB_CELL_FILE",
27568
+ title_prefix: String(row_id),
27569
+ title: col_id,
27570
+ icon: "yi-eye",
27571
+ forget_window: function() {
27572
+ priv.cell_file_win = null;
27573
+ },
27574
+ on_close: function() {
27575
+ teardown_cell_file(gobj);
27576
+ }
27577
+ });
27578
+ if (!presented) return;
27579
+ priv.cell_file_box = $box;
27580
+ priv.cell_file_modal = presented.modal;
27581
+ priv.cell_file_win = presented.win;
27582
+ ids.forEach((id, i) => {
27583
+ request_asset(gobj, id, {
27584
+ target: "popup",
27585
+ box: $box,
27586
+ slot: i
27587
+ });
27588
+ });
27589
+ }
27590
+ /************************************************************
27591
+ * Fill one slot of the file popup with what came back.
27592
+ ************************************************************/
27593
+ function fill_cell_file_slot(gobj, pend, answer, error) {
27594
+ let priv = gobj.priv;
27595
+ if (!priv.cell_file_box || priv.cell_file_box !== pend.box) return;
27596
+ let $item = pend.box.querySelectorAll(".CELL_FILE_ITEM")[pend.slot];
27597
+ let $slot = $item ? $item.querySelector(".CELL_FILE_SLOT") : null;
27598
+ let $caption = $item ? $item.querySelector(".CELL_FILE_CAPTION") : null;
27599
+ if (!$slot || !$caption) {
27600
+ log_error(`${gobj_short_name(gobj)}: file popup has no slot ${pend.slot}`);
27601
+ return;
27602
+ }
27603
+ yui_asset_release($slot);
27604
+ $slot.replaceChildren(yui_asset_element(error ? null : answer, {
27605
+ detail: error || pend.id,
27606
+ alt: pend.id,
27607
+ class: "CELL_FILE_MEDIA"
27608
+ }));
27609
+ refresh_language($slot, t);
27610
+ if (!error && answer) {
27611
+ let meta = [answer.content_type, yui_file_size_label(answer.size)].filter((s) => !!s).join(" · ");
27612
+ if (meta) $caption.appendChild(createElement2([
27613
+ "span",
27614
+ { class: "CELL_FILE_META" },
27615
+ meta
27616
+ ]));
27617
+ let $open = yui_asset_open_link(answer);
27618
+ if ($open) $caption.appendChild($open);
27619
+ }
27620
+ }
27621
+ /************************************************************
27622
+ * Retire the file popup. Called from its on_close -- the
27623
+ * shell or the window has already removed the DOM. The blob
27624
+ * urls it made are revoked; answers still in flight for it
27625
+ * land on nobody (fill_cell_file_slot checks the box).
27626
+ ************************************************************/
27627
+ function teardown_cell_file(gobj) {
27628
+ let priv = gobj.priv;
27629
+ yui_asset_release(priv.cell_file_box);
27630
+ priv.cell_file_box = null;
27631
+ let win = priv.cell_file_win;
27632
+ priv.cell_file_win = null;
27633
+ retire_popup_window(win);
27634
+ priv.cell_file_modal = null;
27635
+ }
27636
+ /************************************************************
27637
+ * Close the file popup (teardown, or a second open).
27638
+ ************************************************************/
27639
+ function close_cell_file_dialog(gobj) {
27640
+ let priv = gobj.priv;
27641
+ if (priv.cell_file_modal) {
27642
+ let modal = priv.cell_file_modal;
27643
+ priv.cell_file_modal = null;
27644
+ modal.close();
27645
+ return;
27646
+ }
27647
+ teardown_cell_file(gobj);
27648
+ }
27649
+ /************************************************************
27014
27650
  * True if the topic's pkey col carries the "rowid" flag.
27015
27651
  ************************************************************/
27016
27652
  function pkey_is_rowid(gobj) {
@@ -27794,6 +28430,57 @@ function ac_show_cell_json(gobj, event, kw, src) {
27794
28430
  return 0;
27795
28431
  }
27796
28432
  /************************************************************
28433
+ * Show the asset(s) one `file` cell names
28434
+ * {
28435
+ * row_id:
28436
+ * col_id:
28437
+ * }
28438
+ ************************************************************/
28439
+ function ac_show_cell_file(gobj, event, kw, src) {
28440
+ open_cell_file_dialog(gobj, kw.row_id, kw.col_id);
28441
+ return 0;
28442
+ }
28443
+ /************************************************************
28444
+ * Hand an asset answer -- or why there is none -- to whoever
28445
+ * asked for it, if it is still there to take it.
28446
+ ************************************************************/
28447
+ function deliver_asset(gobj, pend, answer, error) {
28448
+ let priv = gobj.priv;
28449
+ if (pend.target === "form") {
28450
+ if (!priv.form || priv.form !== pend.form) return;
28451
+ gobj_send_event(priv.form, "EV_SET_FILE_PREVIEW", {
28452
+ name: pend.col,
28453
+ id: pend.id,
28454
+ answer,
28455
+ error
28456
+ }, gobj);
28457
+ return;
28458
+ }
28459
+ fill_cell_file_slot(gobj, pend, answer, error);
28460
+ }
28461
+ /************************************************************
28462
+ * The host answered an asset.
28463
+ * {req_id, answer}
28464
+ ************************************************************/
28465
+ function ac_asset_loaded(gobj, event, kw, src) {
28466
+ let pend = take_pending_asset(gobj, kw.req_id);
28467
+ if (!pend) return 0;
28468
+ deliver_asset(gobj, pend, kw.answer || null, "");
28469
+ return 0;
28470
+ }
28471
+ /************************************************************
28472
+ * The host could not answer it, or nobody did in time.
28473
+ * {req_id, error}
28474
+ ************************************************************/
28475
+ function ac_asset_failed(gobj, event, kw, src) {
28476
+ let why = kw && kw.error || "asset request failed";
28477
+ let pend = take_pending_asset(gobj, kw.req_id);
28478
+ log_error(`${gobj_short_name(gobj)}: asset '${pend ? pend.id : kw.req_id}': ${why}`);
28479
+ if (!pend) return 0;
28480
+ deliver_asset(gobj, pend, null, why);
28481
+ return 0;
28482
+ }
28483
+ /************************************************************
27797
28484
  * One cell of one record changed. Publish it UP as a field write;
27798
28485
  * the host owns the treedb command and its options.
27799
28486
  ************************************************************/
@@ -28297,6 +28984,21 @@ function create_gclass$5(gclass_name) {
28297
28984
  ac_show_cell_json,
28298
28985
  null
28299
28986
  ],
28987
+ [
28988
+ "EV_SHOW_CELL_FILE",
28989
+ ac_show_cell_file,
28990
+ null
28991
+ ],
28992
+ [
28993
+ "EV_ASSET_LOADED",
28994
+ ac_asset_loaded,
28995
+ null
28996
+ ],
28997
+ [
28998
+ "EV_ASSET_FAILED",
28999
+ ac_asset_failed,
29000
+ null
29001
+ ],
28300
29002
  [
28301
29003
  "EV_CHANGE_LOCALE",
28302
29004
  ac_change_locale,
@@ -28397,6 +29099,10 @@ function create_gclass$5(gclass_name) {
28397
29099
  ["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
28398
29100
  ["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
28399
29101
  ["EV_SHOW_CELL_JSON", 0],
29102
+ ["EV_SHOW_CELL_FILE", 0],
29103
+ ["EV_ASSET_LOADED", 0],
29104
+ ["EV_ASSET_FAILED", 0],
29105
+ ["EV_REQUEST_ASSET", event_flag_t.EVF_OUTPUT_EVENT],
28400
29106
  ["EV_CHANGE_LOCALE", 0],
28401
29107
  ["EV_REFRESH", 0],
28402
29108
  ["EV_SHOW_SCHEMA", 0],
@@ -63360,15 +64066,15 @@ function ac_theme$1(gobj, event, kw, src) {
63360
64066
  /************************************************************
63361
64067
  * Zoom controls
63362
64068
  ************************************************************/
63363
- function ac_zoom_in(gobj, event, kw, src) {
64069
+ function ac_zoom_in$1(gobj, event, kw, src) {
63364
64070
  graph_zoom_in(gobj);
63365
64071
  return 0;
63366
64072
  }
63367
- function ac_zoom_out(gobj, event, kw, src) {
64073
+ function ac_zoom_out$1(gobj, event, kw, src) {
63368
64074
  graph_zoom_out(gobj);
63369
64075
  return 0;
63370
64076
  }
63371
- function ac_zoom_reset(gobj, event, kw, src) {
64077
+ function ac_zoom_reset$1(gobj, event, kw, src) {
63372
64078
  let priv = gobj.priv;
63373
64079
  let graph = priv.graph;
63374
64080
  if (priv.anchor_state === "on" && priv.anchor_id) after_camera(gobj, graph.zoomTo(1));
@@ -63756,7 +64462,7 @@ function ac_find_next$1(gobj, event, kw, src) {
63756
64462
  graph_find_step(gobj, !!(kw && kw.back));
63757
64463
  return 0;
63758
64464
  }
63759
- function ac_center(gobj, event, kw, src) {
64465
+ function ac_center$1(gobj, event, kw, src) {
63760
64466
  graph_center(gobj);
63761
64467
  return 0;
63762
64468
  }
@@ -64166,17 +64872,17 @@ function create_gclass$4(gclass_name) {
64166
64872
  ],
64167
64873
  [
64168
64874
  "EV_ZOOM_IN",
64169
- ac_zoom_in,
64875
+ ac_zoom_in$1,
64170
64876
  null
64171
64877
  ],
64172
64878
  [
64173
64879
  "EV_ZOOM_OUT",
64174
- ac_zoom_out,
64880
+ ac_zoom_out$1,
64175
64881
  null
64176
64882
  ],
64177
64883
  [
64178
64884
  "EV_ZOOM_RESET",
64179
- ac_zoom_reset,
64885
+ ac_zoom_reset$1,
64180
64886
  null
64181
64887
  ],
64182
64888
  [
@@ -64276,7 +64982,7 @@ function create_gclass$4(gclass_name) {
64276
64982
  ],
64277
64983
  [
64278
64984
  "EV_CENTER",
64279
- ac_center,
64985
+ ac_center$1,
64280
64986
  null
64281
64987
  ],
64282
64988
  [
@@ -66992,6 +67698,12 @@ function register_c_yui_treedb_graph() {
66992
67698
  * via the host-supplied `node_route`). An alternate landing to the
66993
67699
  * topic cards, in the spirit of "every treedb is a graph".
66994
67700
  *
67701
+ * The CAMERA is the family's (yui_graph_camera.js): the same
67702
+ * toolbar cluster -- zoom in/out, readout, fit, 1:1 -- and the
67703
+ * same wheel: it SCROLLS, and Ctrl + wheel zooms, over the cards
67704
+ * too (yui_graph_forward_wheel). This view zoomed on a bare wheel
67705
+ * and had no toolbar, beside graphs that did neither.
67706
+ *
66995
67707
  * Copyright (c) 2026, ArtGins.
66996
67708
  * All Rights Reserved.
66997
67709
  ***********************************************************************/
@@ -67029,19 +67741,23 @@ var attrs_table$2 = [
67029
67741
  SDATA(data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
67030
67742
  SDATA(data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, "With no `node_route`, PUBLISH a node click as EV_NODE_CLICK instead of dropping it. Opt-in, because a subscriber has to DECLARE the event: the hosts that route by hash, and the ones that want nothing, must not have it appear underneath them"),
67031
67743
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
67744
+ SDATA(data_type_t.DTP_STRING, "wide", 0, "40px", "Height of the toolbar buttons: the family's (C_YUI_TREEDB_GRAPH, C_YUI_JSON_GRAPH, C_YUI_GOBJ_TREE_JS)"),
67032
67745
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
67033
67746
  SDATA_END()
67034
67747
  ];
67035
67748
  var PRIVATE_DATA$2 = {
67036
67749
  $container: null,
67750
+ $canvas: null,
67037
67751
  graph: null,
67038
- theme_observer: null
67752
+ theme_observer: null,
67753
+ unforward_wheel: null
67039
67754
  };
67040
67755
  var __gclass__$2 = null;
67041
67756
  /******************************
67042
67757
  * Framework Methods
67043
67758
  ******************************/
67044
67759
  function mt_create$2(gobj) {
67760
+ ensure_pinch_zoom_patch();
67045
67761
  build_ui$2(gobj);
67046
67762
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
67047
67763
  if (!subscriber) subscriber = gobj_parent(gobj);
@@ -67071,13 +67787,47 @@ function mt_destroy$2(gobj) {
67071
67787
  * Build UI
67072
67788
  ************************************************************/
67073
67789
  function build_ui$2(gobj) {
67790
+ let priv = gobj.priv;
67791
+ const $toolbar = yui_toolbar({}, [
67792
+ [
67793
+ "div",
67794
+ { class: "yui-horizontal-toolbar-section left" },
67795
+ []
67796
+ ],
67797
+ [
67798
+ "div",
67799
+ { class: "yui-horizontal-toolbar-section center" },
67800
+ yui_graph_camera_items(gobj, null, gobj_read_str_attr(gobj, "wide"))
67801
+ ],
67802
+ [
67803
+ "div",
67804
+ { class: "yui-horizontal-toolbar-section right" },
67805
+ []
67806
+ ]
67807
+ ]);
67808
+ refresh_language($toolbar, t);
67809
+ priv.$canvas = createElement2([
67810
+ "div",
67811
+ {
67812
+ class: "TREEDB_SCHEMA_CANVAS",
67813
+ style: "position:relative; flex:1 1 auto; min-height:0;"
67814
+ },
67815
+ []
67816
+ ]);
67074
67817
  let $container = createElement2([
67075
67818
  "div",
67076
67819
  {
67077
67820
  class: "C_YUI_TREEDB_SCHEMA TREEDB_SCHEMA_VIEW",
67078
- style: "position:relative; height:100%; min-height:0;"
67821
+ style: "position:relative; height:100%; min-height:0; display:flex; flex-direction:column;"
67079
67822
  },
67080
- []
67823
+ [[
67824
+ "div",
67825
+ {
67826
+ class: "TREEDB_SCHEMA_TOOLBAR",
67827
+ style: "flex:0 0 auto;"
67828
+ },
67829
+ [$toolbar]
67830
+ ], priv.$canvas]
67081
67831
  ]);
67082
67832
  gobj_write_attr(gobj, "$container", $container);
67083
67833
  }
@@ -67371,7 +68121,10 @@ function edge_style(dark) {
67371
68121
  function build_graph(gobj) {
67372
68122
  let priv = gobj.priv;
67373
68123
  let $container = gobj_read_attr(gobj, "$container");
67374
- if (!$container) return;
68124
+ if (!$container || !priv.$canvas) {
68125
+ log_error(`${gobj_short_name(gobj)}: no UI to draw the schema in`);
68126
+ return;
68127
+ }
67375
68128
  destroy_graph(gobj);
67376
68129
  let data = schema_to_graph(gobj);
67377
68130
  if (data.nodes.length === 0) {
@@ -67383,15 +68136,16 @@ function build_graph(gobj) {
67383
68136
  },
67384
68137
  t("no topics", { defaultValue: "No topics" })
67385
68138
  ]);
67386
- $container.appendChild($empty);
68139
+ priv.$canvas.appendChild($empty);
67387
68140
  return;
67388
68141
  }
67389
68142
  let dark = yui_is_dark();
67390
68143
  let graph;
67391
68144
  try {
67392
68145
  graph = new Graph({
67393
- container: $container,
68146
+ container: priv.$canvas,
67394
68147
  autoResize: true,
68148
+ animation: false,
67395
68149
  data,
67396
68150
  edge: { style: edge_style(dark) },
67397
68151
  layout: {
@@ -67401,8 +68155,8 @@ function build_graph(gobj) {
67401
68155
  ranksep: 90
67402
68156
  },
67403
68157
  behaviors: [
67404
- "zoom-canvas",
67405
68158
  "drag-canvas",
68159
+ ...yui_graph_camera_behaviors(),
67406
68160
  "drag-element"
67407
68161
  ]
67408
68162
  });
@@ -67412,11 +68166,17 @@ function build_graph(gobj) {
67412
68166
  }
67413
68167
  priv.graph = graph;
67414
68168
  graph.setTheme(dark ? "dark" : "light");
68169
+ graph.on("aftertransform", () => {
68170
+ yui_graph_update_zoom($container, priv.graph);
68171
+ });
68172
+ priv.unforward_wheel = yui_graph_forward_wheel(graph, priv.$canvas);
67415
68173
  graph.on(NodeEvent.CLICK, (evt) => {
67416
68174
  let node_id = evt && evt.target && evt.target.id;
67417
68175
  gobj_send_event(gobj, "EV_NODE_CLICK", { node_id }, gobj);
67418
68176
  });
67419
- graph.render().catch((e) => {
68177
+ graph.render().then(() => {
68178
+ yui_graph_update_zoom($container, priv.graph);
68179
+ }).catch((e) => {
67420
68180
  log_error(`${gobj_short_name(gobj)}: schema graph render failed: ${e}`);
67421
68181
  });
67422
68182
  }
@@ -67425,14 +68185,17 @@ function build_graph(gobj) {
67425
68185
  ************************************************************/
67426
68186
  function destroy_graph(gobj) {
67427
68187
  let priv = gobj.priv;
68188
+ if (priv.unforward_wheel) {
68189
+ priv.unforward_wheel();
68190
+ priv.unforward_wheel = null;
68191
+ }
67428
68192
  if (priv.graph) {
67429
68193
  try {
67430
68194
  priv.graph.destroy();
67431
68195
  } catch (e) {}
67432
68196
  priv.graph = null;
67433
68197
  }
67434
- let $container = gobj_read_attr(gobj, "$container");
67435
- if ($container) while ($container.firstChild) $container.removeChild($container.firstChild);
68198
+ if (priv.$canvas) while (priv.$canvas.firstChild) priv.$canvas.removeChild(priv.$canvas.firstChild);
67436
68199
  }
67437
68200
  /***************************
67438
68201
  * Actions
@@ -67522,6 +68285,38 @@ function ac_theme(gobj, event, kw, src) {
67522
68285
  return 0;
67523
68286
  }
67524
68287
  /************************************************************
68288
+ * The camera, from the toolbar. Same moves as the family's
68289
+ * (C_YUI_GOBJ_TREE_JS): +/- 20%, fit, and actual size at the
68290
+ * layout's origin.
68291
+ ************************************************************/
68292
+ function camera_move(gobj, what, promise) {
68293
+ Promise.resolve(promise).then(() => {
68294
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), gobj.priv.graph);
68295
+ }).catch((e) => {
68296
+ log_error(`${gobj_short_name(gobj)}: schema graph ${what} failed: ${e}`);
68297
+ });
68298
+ }
68299
+ function ac_zoom_in(gobj, event, kw, src) {
68300
+ let graph = gobj.priv.graph;
68301
+ if (graph) camera_move(gobj, "zoom in", graph.zoomTo(graph.getZoom() * 1.2));
68302
+ return 0;
68303
+ }
68304
+ function ac_zoom_out(gobj, event, kw, src) {
68305
+ let graph = gobj.priv.graph;
68306
+ if (graph) camera_move(gobj, "zoom out", graph.zoomTo(graph.getZoom() * .8));
68307
+ return 0;
68308
+ }
68309
+ function ac_zoom_reset(gobj, event, kw, src) {
68310
+ let graph = gobj.priv.graph;
68311
+ if (graph && graph.rendered) camera_move(gobj, "actual size", Promise.resolve(graph.zoomTo(1)).then(() => graph.translateTo([0, 0])));
68312
+ return 0;
68313
+ }
68314
+ function ac_center(gobj, event, kw, src) {
68315
+ let graph = gobj.priv.graph;
68316
+ if (graph) camera_move(gobj, "fit", graph.fitView());
68317
+ return 0;
68318
+ }
68319
+ /************************************************************
67525
68320
  * Rebuild from a fresh schema (descs arrived / changed).
67526
68321
  ************************************************************/
67527
68322
  function ac_rebuild(gobj, event, kw, src) {
@@ -67563,13 +68358,37 @@ function create_gclass$2(gclass_name) {
67563
68358
  "EV_REBUILD",
67564
68359
  ac_rebuild,
67565
68360
  null
68361
+ ],
68362
+ [
68363
+ "EV_ZOOM_IN",
68364
+ ac_zoom_in,
68365
+ null
68366
+ ],
68367
+ [
68368
+ "EV_ZOOM_OUT",
68369
+ ac_zoom_out,
68370
+ null
68371
+ ],
68372
+ [
68373
+ "EV_ZOOM_RESET",
68374
+ ac_zoom_reset,
68375
+ null
68376
+ ],
68377
+ [
68378
+ "EV_CENTER",
68379
+ ac_center,
68380
+ null
67566
68381
  ]
67567
68382
  ]]];
67568
68383
  const event_types = [
67569
68384
  ["EV_NODE_CLICK", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
67570
68385
  ["EV_SHOW", 0],
67571
68386
  ["EV_THEME", 0],
67572
- ["EV_REBUILD", 0]
68387
+ ["EV_REBUILD", 0],
68388
+ ["EV_ZOOM_IN", 0],
68389
+ ["EV_ZOOM_OUT", 0],
68390
+ ["EV_ZOOM_RESET", 0],
68391
+ ["EV_CENTER", 0]
67573
68392
  ];
67574
68393
  __gclass__$2 = gclass_create(gclass_name, event_types, states, gmt$2, 0, attrs_table$2, PRIVATE_DATA$2, 0, 0, 0, 0);
67575
68394
  if (!__gclass__$2) return -1;