@yuneta/gobj-ui 7.23.59 → 7.23.60

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.
@@ -5827,6 +5827,414 @@ function register_c_yui_wizard() {
5827
5827
  return create_gclass$17(GCLASS_NAME$17);
5828
5828
  }
5829
5829
  //#endregion
5830
+ //#region src/yui_file_field.js
5831
+ /***********************************************************************
5832
+ * yui_file_field.js
5833
+ *
5834
+ * The control that PICKS a file for a treedb `file` column, and the
5835
+ * manifest that carries its bytes beside the record.
5836
+ *
5837
+ * A column flagged ['fkey','file'] holds an fkey into the treedb's
5838
+ * system topic `__assets__`: the bytes live on disk under the treedb,
5839
+ * the index in memory, and the record names the asset by its sha256.
5840
+ * So this control does NOT put a file in the record. It keeps the
5841
+ * `File` the person picked and hands up an IDENTITY, and the bytes
5842
+ * travel beside the record in a `__files__` manifest keyed by column.
5843
+ *
5844
+ * Three things about that are not obvious and are the whole reason
5845
+ * this module is separate:
5846
+ *
5847
+ * - **The file is read at SAVE, not at pick.** An `<input type=file>`
5848
+ * hands over a `File`, which is a REFERENCE and not the bytes.
5849
+ * Picking shows a name and a size and reads nothing, so cancelling
5850
+ * a form does not mean a 40 MB video was read for nothing.
5851
+ * - **A `File` cannot travel in a kw.** A kw is plain json -- the
5852
+ * machine trace serialises it -- and a `File` is a host object. The
5853
+ * form KEEPS it and the host asks for it through a local method.
5854
+ * - **Reading is a promise, so saving stops being synchronous**, and
5855
+ * a resolved promise is an OS notification that must enter the
5856
+ * machine as an EVENT, never as a chain of callbacks. That is the
5857
+ * HOST's business; what lives here is the promise it awaits.
5858
+ *
5859
+ * The pure half (manifest, size, sha256, the name of a picked file)
5860
+ * is tested without a DOM; the DOM half builds the control.
5861
+ *
5862
+ * Using Bulma Framework (https://bulma.io)
5863
+ *
5864
+ * Copyright (c) 2026, ArtGins.
5865
+ * All Rights Reserved.
5866
+ ***********************************************************************/
5867
+ var YUI_FILE_ACCEPT = [
5868
+ "image/jpeg",
5869
+ "image/png",
5870
+ "image/webp",
5871
+ "image/gif",
5872
+ "application/pdf",
5873
+ "video/mp4",
5874
+ "video/webm",
5875
+ "video/quicktime",
5876
+ "video/ogg",
5877
+ "video/x-matroska",
5878
+ "audio/mpeg",
5879
+ "audio/mp4",
5880
+ "audio/ogg",
5881
+ "audio/wav",
5882
+ "audio/webm",
5883
+ "audio/flac"
5884
+ ].join(",");
5885
+ /***************************
5886
+ * The pure half
5887
+ ***************************/
5888
+ /***************************************************************************
5889
+ * A size a person reads. Bytes up to 1 KB, then one decimal.
5890
+ ***************************************************************************/
5891
+ function yui_file_size_label(bytes) {
5892
+ let n = Number(bytes);
5893
+ if (!isFinite(n) || n < 0) return "";
5894
+ if (n < 1024) return `${n} B`;
5895
+ const units = [
5896
+ "KB",
5897
+ "MB",
5898
+ "GB",
5899
+ "TB"
5900
+ ];
5901
+ let i = -1;
5902
+ do {
5903
+ n = n / 1024;
5904
+ i++;
5905
+ } while (n >= 1024 && i < units.length - 1);
5906
+ return `${n.toFixed(1)} ${units[i]}`;
5907
+ }
5908
+ /***************************************************************************
5909
+ * The sha256 of an ArrayBuffer, lowercase hex, or null.
5910
+ *
5911
+ * NULL is a legal answer and not a failure: `crypto.subtle` exists only
5912
+ * in a SECURE CONTEXT, so a dev server on plain http has none. Then the
5913
+ * record carries no id and treedb fills it -- the write path hashes what
5914
+ * arrives anyway, because it never trusts the client's id. What the hash
5915
+ * buys here is an integrity check the backend can make (a wrong id with
5916
+ * good bytes is refused), not the identity itself.
5917
+ ***************************************************************************/
5918
+ async function yui_file_sha256(buffer) {
5919
+ const subtle = typeof crypto !== "undefined" && crypto.subtle;
5920
+ if (!subtle) return null;
5921
+ try {
5922
+ const digest = await subtle.digest("SHA-256", buffer);
5923
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
5924
+ } catch (e) {
5925
+ log_error(`cannot hash the picked file: ${e && e.message ? e.message : e}`);
5926
+ return null;
5927
+ }
5928
+ }
5929
+ /***************************************************************************
5930
+ * base64 of an ArrayBuffer.
5931
+ *
5932
+ * In chunks because `String.fromCharCode(...bytes)` spreads the whole
5933
+ * array onto the call stack, and a few hundred KB is already a
5934
+ * RangeError -- which for a file picker is every file that matters.
5935
+ ***************************************************************************/
5936
+ function yui_array_buffer_to_base64(buffer) {
5937
+ const bytes = new Uint8Array(buffer);
5938
+ const CHUNK = 32768;
5939
+ let binary = "";
5940
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
5941
+ return btoa(binary);
5942
+ }
5943
+ /***************************************************************************
5944
+ * Read ONE picked file into what the manifest carries.
5945
+ *
5946
+ * -> {content64, content_type, original_name, size, id}
5947
+ *
5948
+ * `id` is the sha256 when it could be computed, and "" when it could
5949
+ * not. Nothing else in here is optional: treedb checks the size on the
5950
+ * base64 before decoding it, and the type against the BYTES.
5951
+ ***************************************************************************/
5952
+ async function yui_file_read(file) {
5953
+ const buffer = await file.arrayBuffer();
5954
+ const id = await yui_file_sha256(buffer);
5955
+ return {
5956
+ content64: yui_array_buffer_to_base64(buffer),
5957
+ content_type: file.type || "",
5958
+ original_name: file.name || "",
5959
+ size: file.size,
5960
+ id: id || ""
5961
+ };
5962
+ }
5963
+ /***************************************************************************
5964
+ * The write a record with picked files becomes.
5965
+ *
5966
+ * picks {col: read} what yui_file_read() answered, per column
5967
+ * record the record the form collected (MODIFIED: each picked
5968
+ * column takes the id, or "" when there is none)
5969
+ *
5970
+ * -> {record, __files__}
5971
+ *
5972
+ * `__files__` is not a column: it is an instruction to the write path,
5973
+ * consumed and dropped at the door. The bytes never go INSIDE the
5974
+ * column -- a field that CARRIES the bytes and a field that KEEPS them
5975
+ * are one word apart and 460 MB of RAM apart.
5976
+ ***************************************************************************/
5977
+ function yui_files_manifest(picks, record) {
5978
+ const out = record || {};
5979
+ const manifest = {};
5980
+ Object.keys(picks || {}).forEach((col) => {
5981
+ const read = picks[col];
5982
+ if (!read) return;
5983
+ manifest[col] = {
5984
+ content64: read.content64,
5985
+ content_type: read.content_type,
5986
+ original_name: read.original_name
5987
+ };
5988
+ out[col] = read.id || "";
5989
+ });
5990
+ return {
5991
+ record: out,
5992
+ __files__: manifest
5993
+ };
5994
+ }
5995
+ /***************************
5996
+ * The DOM half
5997
+ ***************************/
5998
+ /***************************************************************************
5999
+ * What the control shows for what the column HOLDS today: the asset id,
6000
+ * shortened. A sha256 in full is 64 characters of noise in a form.
6001
+ ***************************************************************************/
6002
+ function yui_file_id_label(id) {
6003
+ if (!id) return "";
6004
+ return id.length > 16 ? `${id.slice(0, 8)}…${id.slice(-4)}` : id;
6005
+ }
6006
+ /***************************************************************************
6007
+ * The control for a `file` column.
6008
+ *
6009
+ * name the column
6010
+ * value the asset id the record holds today ("" when none)
6011
+ * readonly draw it, do not let it change
6012
+ * on_pick (file|null) -> void. NULL is "the person cleared it".
6013
+ *
6014
+ * Returns the `<div class="control">` content, and hangs the state the
6015
+ * form reads off the returned element:
6016
+ *
6017
+ * $control.yui_file the picked File, or null
6018
+ * $control.yui_file_value the id the column keeps ("" = cleared)
6019
+ *
6020
+ * The `<input type="file">` is hidden and driven by a button, because a
6021
+ * bare file input cannot be styled and says "Sin archivos seleccionados"
6022
+ * in the browser's own language, not the app's.
6023
+ ***************************************************************************/
6024
+ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
6025
+ const has_asset = !!value;
6026
+ const $input = createElement2(["input", {
6027
+ type: "file",
6028
+ class: "FILE_INPUT",
6029
+ name: `${name}__file`,
6030
+ accept: accept || YUI_FILE_ACCEPT,
6031
+ hidden: ""
6032
+ }]);
6033
+ const $pick = createElement2([
6034
+ "button",
6035
+ {
6036
+ type: "button",
6037
+ class: "FILE_PICK button is-small",
6038
+ "data-i18n-title": "choose a file",
6039
+ "aria-label": "choose a file"
6040
+ },
6041
+ [["span", { class: "FILE_PICK_ICON yi-upload" }], [
6042
+ "span",
6043
+ {
6044
+ class: "FILE_PICK_LABEL",
6045
+ i18n: "choose a file"
6046
+ },
6047
+ "choose a file"
6048
+ ]]
6049
+ ]);
6050
+ const $clear = createElement2([
6051
+ "button",
6052
+ {
6053
+ type: "button",
6054
+ class: "FILE_CLEAR button is-small",
6055
+ "data-i18n-title": "remove the file",
6056
+ "aria-label": "remove the file"
6057
+ },
6058
+ [["span", { class: "FILE_CLEAR_ICON yi-trash" }]]
6059
+ ]);
6060
+ const $name = createElement2(["span", { class: "FILE_NAME" }]);
6061
+ const $size = createElement2(["span", { class: "FILE_SIZE" }]);
6062
+ const $state = createElement2(["span", { class: "FILE_STATE" }]);
6063
+ const $control = createElement2([
6064
+ "div",
6065
+ { class: "FILE_FIELD" },
6066
+ [
6067
+ $input,
6068
+ $pick,
6069
+ $name,
6070
+ $size,
6071
+ $state,
6072
+ $clear
6073
+ ]
6074
+ ]);
6075
+ $control.yui_file = null;
6076
+ $control.yui_file_value = value || "";
6077
+ const render = () => {
6078
+ const picked = $control.yui_file;
6079
+ const kept = $control.yui_file_value;
6080
+ $name.textContent = picked ? picked.name || "" : yui_file_id_label(kept);
6081
+ $size.textContent = picked ? yui_file_size_label(picked.size) : "";
6082
+ $state.className = "FILE_STATE";
6083
+ $state.removeAttribute("data-i18n");
6084
+ $state.textContent = "";
6085
+ if (picked) {
6086
+ $state.classList.add("is-picked");
6087
+ $state.setAttribute("data-i18n", "not saved yet");
6088
+ $state.textContent = "not saved yet";
6089
+ } else if (!kept) {
6090
+ $state.classList.add("is-empty");
6091
+ $state.setAttribute("data-i18n", "no file");
6092
+ $state.textContent = "no file";
6093
+ }
6094
+ $clear.hidden = readonly || !picked && !kept;
6095
+ $pick.hidden = !!readonly;
6096
+ };
6097
+ if (!readonly) {
6098
+ $pick.addEventListener("click", () => {
6099
+ $input.click();
6100
+ });
6101
+ $input.addEventListener("change", () => {
6102
+ const file = $input.files && $input.files.length ? $input.files[0] : null;
6103
+ $control.yui_file = file;
6104
+ render();
6105
+ if (on_pick) on_pick(file);
6106
+ });
6107
+ $clear.addEventListener("click", () => {
6108
+ $control.yui_file = null;
6109
+ $control.yui_file_value = "";
6110
+ $input.value = "";
6111
+ render();
6112
+ if (on_pick) on_pick(null);
6113
+ });
6114
+ }
6115
+ $control.yui_file_render = render;
6116
+ render();
6117
+ return {
6118
+ $control,
6119
+ $input,
6120
+ has_asset
6121
+ };
6122
+ }
6123
+ //#endregion
6124
+ //#region src/yui_asset.js
6125
+ function ref_to_id(ref) {
6126
+ if (!ref) return null;
6127
+ if (typeof ref === "string") {
6128
+ if (ref.indexOf("^") < 0) return ref;
6129
+ const parts = ref.split("^");
6130
+ return parts.length >= 2 && parts[1] ? parts[1] : null;
6131
+ }
6132
+ if (typeof ref === "object" && typeof ref.id === "string") return ref.id;
6133
+ return null;
6134
+ }
6135
+ /***************************************************************************
6136
+ * Every asset id a column names, in order. Always an array, possibly empty.
6137
+ ***************************************************************************/
6138
+ function yui_asset_ids(ref) {
6139
+ if (Array.isArray(ref)) return ref.map(ref_to_id).filter((id) => !!id);
6140
+ const id = ref_to_id(ref);
6141
+ return id ? [id] : [];
6142
+ }
6143
+ /***************************************************************************
6144
+ * The first asset id a column names, or null.
6145
+ ***************************************************************************/
6146
+ function yui_asset_id(ref) {
6147
+ const ids = yui_asset_ids(ref);
6148
+ return ids.length ? ids[0] : null;
6149
+ }
6150
+ /***************************************************************************
6151
+ * Turn a `get-asset` answer into something an element can load.
6152
+ *
6153
+ * Returns null when the answer carries neither -- which the caller must
6154
+ * treat as "missing", never as an empty src: an <img src=""> reloads the
6155
+ * PAGE in some browsers.
6156
+ ***************************************************************************/
6157
+ function yui_asset_src(answer) {
6158
+ if (!answer) return null;
6159
+ if (answer.mode === "url" && answer.url) return answer.url;
6160
+ if (answer.mode === "inline" && answer.content64) return `data:${answer.content_type || "application/octet-stream"};base64,${answer.content64}`;
6161
+ return null;
6162
+ }
6163
+ /***************************************************************************
6164
+ * The marker for an asset that is not there.
6165
+ *
6166
+ * A missing image used to leave a broken box and no word about it, which
6167
+ * is indistinguishable from a slow one and from a bug. This says so, and
6168
+ * it says WHICH: `detail` is data (a name, a path) and is never
6169
+ * translated, while the label carries its i18n key so a language change
6170
+ * re-translates it.
6171
+ ***************************************************************************/
6172
+ function yui_asset_missing(detail, opts = {}) {
6173
+ const key = opts.key || "asset not available";
6174
+ const children = [["span", { class: "ASSET_MISSING_ICON yi-triangle-exclamation" }], [
6175
+ "span",
6176
+ {
6177
+ class: "ASSET_MISSING_LABEL",
6178
+ i18n: key
6179
+ },
6180
+ key
6181
+ ]];
6182
+ if (detail) children.push([
6183
+ "span",
6184
+ { class: "ASSET_MISSING_DETAIL" },
6185
+ String(detail)
6186
+ ]);
6187
+ const attrs = { class: `ASSET_MISSING ${opts.class || ""}`.trim() };
6188
+ if (detail) attrs.title = String(detail);
6189
+ else attrs["data-i18n-title"] = key;
6190
+ return createElement2([
6191
+ "div",
6192
+ attrs,
6193
+ children
6194
+ ]);
6195
+ }
6196
+ /***************************************************************************
6197
+ * The element for one asset.
6198
+ *
6199
+ * The KIND comes from the content type the backend stored, not from the
6200
+ * name a person typed: video and audio are assets too, and an <img> whose
6201
+ * src is a film shows the broken box this module exists to remove.
6202
+ *
6203
+ * `detail` is what the missing marker shows -- pass the original name or
6204
+ * the source path, the thing a person can act on.
6205
+ ***************************************************************************/
6206
+ function yui_asset_element(answer, opts = {}) {
6207
+ const src = yui_asset_src(answer);
6208
+ if (!src) return yui_asset_missing(opts.detail, opts);
6209
+ const ct = (answer.content_type || "").toLowerCase();
6210
+ const cls = `ASSET_MEDIA ${opts.class || ""}`.trim();
6211
+ let el;
6212
+ if (ct.startsWith("video/")) el = createElement2(["video", {
6213
+ class: cls,
6214
+ controls: "",
6215
+ preload: "metadata"
6216
+ }]);
6217
+ else if (ct.startsWith("audio/")) el = createElement2(["audio", {
6218
+ class: cls,
6219
+ controls: "",
6220
+ preload: "metadata"
6221
+ }]);
6222
+ else {
6223
+ el = createElement2(["img", {
6224
+ class: cls,
6225
+ loading: "lazy"
6226
+ }]);
6227
+ el.alt = opts.alt || opts.detail || "";
6228
+ }
6229
+ el.src = src;
6230
+ el.onerror = () => {
6231
+ log_error(`asset could not be loaded: ${opts.detail || src}`);
6232
+ const $marker = yui_asset_missing(opts.detail, opts);
6233
+ if (el.parentNode) el.parentNode.replaceChild($marker, el);
6234
+ };
6235
+ return el;
6236
+ }
6237
+ //#endregion
5830
6238
  //#region src/yui_inputs.js
5831
6239
  /***************************************************************
5832
6240
  * The ✕ shows only when there is something to clear AND the field
@@ -6800,6 +7208,9 @@ function build_form_field_conf(gobj, field_desc) {
6800
7208
  field_conf.tag = "select2";
6801
7209
  field_conf.options = get_fkey_select_options(gobj, field_desc);
6802
7210
  break;
7211
+ case "file":
7212
+ field_conf.tag = "file";
7213
+ break;
6803
7214
  case "image":
6804
7215
  case "icon":
6805
7216
  case "string":
@@ -7485,6 +7896,17 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
7485
7896
  $control.appendChild($extend);
7486
7897
  break;
7487
7898
  }
7899
+ case "file":
7900
+ $extend = yui_file_control(gobj, {
7901
+ name,
7902
+ value: "",
7903
+ readonly,
7904
+ on_pick: function() {
7905
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
7906
+ }
7907
+ }).$control;
7908
+ $control.appendChild($extend);
7909
+ break;
7488
7910
  default:
7489
7911
  $extend = createElement2([
7490
7912
  "strong",
@@ -7985,6 +8407,9 @@ function get_form_values(gobj, $form) {
7985
8407
  case "radio":
7986
8408
  value = getInputValue($input);
7987
8409
  break;
8410
+ case "file":
8411
+ value = $input.yui_file_value || "";
8412
+ break;
7988
8413
  case "jsoneditor":
7989
8414
  let jsoneditor = $input.jsoneditor;
7990
8415
  if (jsoneditor) {
@@ -8041,6 +8466,13 @@ function clear_data(gobj, $form) {
8041
8466
  else $input.value = null;
8042
8467
  }
8043
8468
  });
8469
+ $form.querySelectorAll(".FILE_FIELD").forEach(($file) => {
8470
+ $file.yui_file = null;
8471
+ $file.yui_file_value = "";
8472
+ let $picker = $file.querySelector(".FILE_INPUT");
8473
+ if ($picker) $picker.value = "";
8474
+ if ($file.yui_file_render) $file.yui_file_render();
8475
+ });
8044
8476
  $form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
8045
8477
  if ($table.tabulator.initialized) $table.tabulator.clearData();
8046
8478
  $table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
@@ -8110,6 +8542,11 @@ function set_form_values(gobj, template, $form, record) {
8110
8542
  value = treedb_value_2_form_value(gobj, field_desc, value);
8111
8543
  if (value === void 0) value = null;
8112
8544
  switch (tag) {
8545
+ case "file":
8546
+ $input.yui_file = null;
8547
+ $input.yui_file_value = yui_asset_id(value) || "";
8548
+ if ($input.yui_file_render) $input.yui_file_render();
8549
+ break;
8113
8550
  case "input":
8114
8551
  switch (input_type) {
8115
8552
  case "datetime-local":
@@ -8538,11 +8975,63 @@ function ac_timeout(gobj, event, kw, src) {
8538
8975
  /***************************
8539
8976
  * FSM
8540
8977
  ***************************/
8978
+ /************************************************************
8979
+ * Framework Method command
8980
+ ************************************************************/
8981
+ function mt_command_parser$2(gobj, command, kw, src) {
8982
+ switch (command) {
8983
+ case "get_picked_files": return cmd_get_picked_files(gobj, command, kw, src);
8984
+ default:
8985
+ log_error(`${gobj_short_name(gobj)}: command not found: ${command}`);
8986
+ return {
8987
+ result: -1,
8988
+ comment: `Command not found: ${command}`,
8989
+ schema: null,
8990
+ data: null
8991
+ };
8992
+ }
8993
+ }
8994
+ /************************************************************
8995
+ * The files a person picked and the record cannot carry.
8996
+ *
8997
+ * A `File` is a host object and a kw is plain json -- the machine trace
8998
+ * serialises it -- so the picked files never travel in an event. The
8999
+ * form KEEPS them and the HOST asks for them here, at save, and turns
9000
+ * them into the `__files__` manifest that rides beside the record.
9001
+ *
9002
+ * Answers `data: {col: File}`, empty when nothing was picked, which is
9003
+ * the ordinary case and not an error.
9004
+ ************************************************************/
9005
+ function cmd_get_picked_files(gobj, cmd, kw, src) {
9006
+ const files = {};
9007
+ const $container = gobj_read_attr(gobj, "$container");
9008
+ const $form = $container ? $container.querySelector("form") : null;
9009
+ if (!$form) {
9010
+ log_error(`${gobj_short_name(gobj)}: no form to read the picked files from`);
9011
+ return {
9012
+ result: -1,
9013
+ comment: "no form",
9014
+ schema: null,
9015
+ data: null
9016
+ };
9017
+ }
9018
+ $form.querySelectorAll(".yui-form-data-input").forEach(($input) => {
9019
+ if ($input.dataset.form_tag !== "file") return;
9020
+ if ($input.yui_file) files[$input.dataset.treedb_name] = $input.yui_file;
9021
+ });
9022
+ return {
9023
+ result: 0,
9024
+ comment: "",
9025
+ schema: null,
9026
+ data: files
9027
+ };
9028
+ }
8541
9029
  var gmt$16 = {
8542
9030
  mt_create: mt_create$16,
8543
9031
  mt_start: mt_start$15,
8544
9032
  mt_stop: mt_stop$15,
8545
- mt_destroy: mt_destroy$16
9033
+ mt_destroy: mt_destroy$16,
9034
+ mt_command_parser: mt_command_parser$2
8546
9035
  };
8547
9036
  /***************************************************************
8548
9037
  * Create the GClass
@@ -23985,6 +24474,9 @@ function create_tabulator(gobj) {
23985
24474
  vertAlign = "middle";
23986
24475
  colFormatter = icon_formatter;
23987
24476
  break;
24477
+ case "file":
24478
+ colFormatter = file_cell_formatter;
24479
+ break;
23988
24480
  case "object":
23989
24481
  case "dict":
23990
24482
  case "template":
@@ -24229,6 +24721,33 @@ function table__destroy(gobj) {
24229
24721
  * render a solid square -- a glyph nobody recognises rather
24230
24722
  * than a mistake somebody can fix.
24231
24723
  ************************************************************/
24724
+ /************************************************************
24725
+ * What a `file` column shows in a table cell: the asset it names.
24726
+ *
24727
+ * A sha256 in full is 64 characters of noise in a column, so it is
24728
+ * shortened and the whole of it is the tooltip. Empty is SAID, not left
24729
+ * blank -- a blank cell and a cell whose asset failed to load look the
24730
+ * same, and only one of them is normal.
24731
+ ************************************************************/
24732
+ function file_cell_formatter(cell, formatterParams, onRendered) {
24733
+ const id = yui_asset_id(cell.getValue());
24734
+ if (!id) return createElement2([
24735
+ "span",
24736
+ {
24737
+ class: "FILE_CELL is-empty",
24738
+ "data-i18n": "no file"
24739
+ },
24740
+ "no file"
24741
+ ]);
24742
+ return createElement2([
24743
+ "span",
24744
+ {
24745
+ class: "FILE_CELL",
24746
+ title: id
24747
+ },
24748
+ yui_file_id_label(id)
24749
+ ]);
24750
+ }
24232
24751
  function icon_formatter(cell) {
24233
24752
  let name = cell.getValue();
24234
24753
  if (!is_string(name) || empty_string(name)) return "";
@@ -24324,6 +24843,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
24324
24843
  ].join("");
24325
24844
  else value = "";
24326
24845
  break;
24846
+ case "file":
24327
24847
  case "fkey":
24328
24848
  let new_value = [];
24329
24849
  if (value) {
@@ -24848,6 +25368,7 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation) {
24848
25368
  case "hook":
24849
25369
  value = null;
24850
25370
  break;
25371
+ case "file": break;
24851
25372
  case "fkey":
24852
25373
  value = build_fkey_ref(gobj, col, value);
24853
25374
  break;
@@ -25331,13 +25852,71 @@ function ac_paste_rows(gobj, event, kw, src) {
25331
25852
  function ac_form_save_record(gobj, event, kw, src) {
25332
25853
  if (refuse_if_readonly$2(gobj, event)) return -1;
25333
25854
  let mode = gobj_read_str_attr(src, "form_mode");
25334
- gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
25855
+ let picked = {};
25856
+ const answer = gobj_command(src, "get_picked_files", {}, gobj);
25857
+ if (answer && answer.result >= 0 && answer.data) picked = answer.data;
25858
+ const cols = Object.keys(picked);
25859
+ if (cols.length === 0) {
25860
+ publish_treedb_write(gobj, mode, kw);
25861
+ setTimeout(function() {
25862
+ close_form_dialog(gobj);
25863
+ }, 0);
25864
+ return 0;
25865
+ }
25866
+ Promise.all(cols.map((col) => {
25867
+ return yui_file_read(picked[col]).then((read) => {
25868
+ return {
25869
+ col,
25870
+ read
25871
+ };
25872
+ });
25873
+ })).then((reads) => {
25874
+ const picks = {};
25875
+ reads.forEach((r) => {
25876
+ picks[r.col] = r.read;
25877
+ });
25878
+ gobj_send_event(gobj, "EV_FILES_READ", {
25879
+ mode,
25880
+ record: kw,
25881
+ picks
25882
+ }, gobj);
25883
+ }).catch((e) => {
25884
+ gobj_send_event(gobj, "EV_FILES_FAILED", { reason: e && e.message ? e.message : String(e) }, gobj);
25885
+ });
25886
+ return 0;
25887
+ }
25888
+ /************************************************************
25889
+ * The one place a write of this view leaves for the app.
25890
+ ************************************************************/
25891
+ function publish_treedb_write(gobj, mode, record, jn_files) {
25892
+ const kw_write = {
25335
25893
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
25336
- record: kw
25894
+ record
25895
+ };
25896
+ if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
25897
+ gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
25898
+ }
25899
+ /************************************************************
25900
+ * {mode, record, picks} -- the bytes are here now
25901
+ ************************************************************/
25902
+ function ac_files_read(gobj, event, kw, src) {
25903
+ const { record, __files__ } = yui_files_manifest(kw.picks, kw.record);
25904
+ publish_treedb_write(gobj, kw.mode, record, __files__);
25905
+ close_form_dialog(gobj);
25906
+ return 0;
25907
+ }
25908
+ /************************************************************
25909
+ * {reason} -- the browser could not read what was picked
25910
+ *
25911
+ * The form stays OPEN: the record is still there, and closing it would
25912
+ * throw away what the person typed for a failure that is not theirs.
25913
+ ************************************************************/
25914
+ function ac_files_failed(gobj, event, kw, src) {
25915
+ log_error(`${gobj_short_name(gobj)}: cannot read the picked file: ${kw.reason}`);
25916
+ yui_shell_confirm_ok(yui_shell_of(gobj), "cannot read the picked file", {
25917
+ t,
25918
+ ok_label: "accept"
25337
25919
  });
25338
- setTimeout(function() {
25339
- close_form_dialog(gobj);
25340
- }, 0);
25341
25920
  return 0;
25342
25921
  }
25343
25922
  /************************************************************
@@ -25856,6 +26435,16 @@ function create_gclass$5(gclass_name) {
25856
26435
  ac_form_save_record,
25857
26436
  null
25858
26437
  ],
26438
+ [
26439
+ "EV_FILES_READ",
26440
+ ac_files_read,
26441
+ null
26442
+ ],
26443
+ [
26444
+ "EV_FILES_FAILED",
26445
+ ac_files_failed,
26446
+ null
26447
+ ],
25859
26448
  [
25860
26449
  "EV_NEW_ROW",
25861
26450
  ac_new_row,
@@ -25990,6 +26579,8 @@ function create_gclass$5(gclass_name) {
25990
26579
  ["EV_NODE_DELETED", 0],
25991
26580
  ["EV_EDITION_MODE", 0],
25992
26581
  ["EV_SAVE_RECORD", 0],
26582
+ ["EV_FILES_READ", 0],
26583
+ ["EV_FILES_FAILED", 0],
25993
26584
  ["EV_NEW_ROW", 0],
25994
26585
  ["EV_DELETE_ROWS", 0],
25995
26586
  ["EV_COPY_ROWS", 0],
@@ -67502,120 +68093,6 @@ function register_c_yui_period() {
67502
68093
  return create_gclass(GCLASS_NAME);
67503
68094
  }
67504
68095
  //#endregion
67505
- //#region src/yui_asset.js
67506
- function ref_to_id(ref) {
67507
- if (!ref) return null;
67508
- if (typeof ref === "string") {
67509
- if (ref.indexOf("^") < 0) return ref;
67510
- const parts = ref.split("^");
67511
- return parts.length >= 2 && parts[1] ? parts[1] : null;
67512
- }
67513
- if (typeof ref === "object" && typeof ref.id === "string") return ref.id;
67514
- return null;
67515
- }
67516
- /***************************************************************************
67517
- * Every asset id a column names, in order. Always an array, possibly empty.
67518
- ***************************************************************************/
67519
- function yui_asset_ids(ref) {
67520
- if (Array.isArray(ref)) return ref.map(ref_to_id).filter((id) => !!id);
67521
- const id = ref_to_id(ref);
67522
- return id ? [id] : [];
67523
- }
67524
- /***************************************************************************
67525
- * The first asset id a column names, or null.
67526
- ***************************************************************************/
67527
- function yui_asset_id(ref) {
67528
- const ids = yui_asset_ids(ref);
67529
- return ids.length ? ids[0] : null;
67530
- }
67531
- /***************************************************************************
67532
- * Turn a `get-asset` answer into something an element can load.
67533
- *
67534
- * Returns null when the answer carries neither -- which the caller must
67535
- * treat as "missing", never as an empty src: an <img src=""> reloads the
67536
- * PAGE in some browsers.
67537
- ***************************************************************************/
67538
- function yui_asset_src(answer) {
67539
- if (!answer) return null;
67540
- if (answer.mode === "url" && answer.url) return answer.url;
67541
- if (answer.mode === "inline" && answer.content64) return `data:${answer.content_type || "application/octet-stream"};base64,${answer.content64}`;
67542
- return null;
67543
- }
67544
- /***************************************************************************
67545
- * The marker for an asset that is not there.
67546
- *
67547
- * A missing image used to leave a broken box and no word about it, which
67548
- * is indistinguishable from a slow one and from a bug. This says so, and
67549
- * it says WHICH: `detail` is data (a name, a path) and is never
67550
- * translated, while the label carries its i18n key so a language change
67551
- * re-translates it.
67552
- ***************************************************************************/
67553
- function yui_asset_missing(detail, opts = {}) {
67554
- const key = opts.key || "asset not available";
67555
- const children = [["span", { class: "ASSET_MISSING_ICON yi-triangle-exclamation" }], [
67556
- "span",
67557
- {
67558
- class: "ASSET_MISSING_LABEL",
67559
- i18n: key
67560
- },
67561
- key
67562
- ]];
67563
- if (detail) children.push([
67564
- "span",
67565
- { class: "ASSET_MISSING_DETAIL" },
67566
- String(detail)
67567
- ]);
67568
- const attrs = { class: `ASSET_MISSING ${opts.class || ""}`.trim() };
67569
- if (detail) attrs.title = String(detail);
67570
- else attrs["data-i18n-title"] = key;
67571
- return createElement2([
67572
- "div",
67573
- attrs,
67574
- children
67575
- ]);
67576
- }
67577
- /***************************************************************************
67578
- * The element for one asset.
67579
- *
67580
- * The KIND comes from the content type the backend stored, not from the
67581
- * name a person typed: video and audio are assets too, and an <img> whose
67582
- * src is a film shows the broken box this module exists to remove.
67583
- *
67584
- * `detail` is what the missing marker shows -- pass the original name or
67585
- * the source path, the thing a person can act on.
67586
- ***************************************************************************/
67587
- function yui_asset_element(answer, opts = {}) {
67588
- const src = yui_asset_src(answer);
67589
- if (!src) return yui_asset_missing(opts.detail, opts);
67590
- const ct = (answer.content_type || "").toLowerCase();
67591
- const cls = `ASSET_MEDIA ${opts.class || ""}`.trim();
67592
- let el;
67593
- if (ct.startsWith("video/")) el = createElement2(["video", {
67594
- class: cls,
67595
- controls: "",
67596
- preload: "metadata"
67597
- }]);
67598
- else if (ct.startsWith("audio/")) el = createElement2(["audio", {
67599
- class: cls,
67600
- controls: "",
67601
- preload: "metadata"
67602
- }]);
67603
- else {
67604
- el = createElement2(["img", {
67605
- class: cls,
67606
- loading: "lazy"
67607
- }]);
67608
- el.alt = opts.alt || opts.detail || "";
67609
- }
67610
- el.src = src;
67611
- el.onerror = () => {
67612
- log_error(`asset could not be loaded: ${opts.detail || src}`);
67613
- const $marker = yui_asset_missing(opts.detail, opts);
67614
- if (el.parentNode) el.parentNode.replaceChild($marker, el);
67615
- };
67616
- return el;
67617
- }
67618
- //#endregion
67619
68096
  //#region src/dev_machine_trace.js
67620
68097
  /***********************************************************************
67621
68098
  * dev_machine_trace.js
@@ -69299,6 +69776,6 @@ function setup_frontend_view(self) {
69299
69776
  return win;
69300
69777
  }
69301
69778
  //#endregion
69302
- export { EditControl, MarkerControl, YUI_PERIODS, YUI_PERIODS_DEFAULT, YUI_ROLLING, addClasses, apply_dev_traces, attach_clear, build_dev_panel, dev_window_was_open, disableElements, enableElements, epoch_to_local_input, epoch_to_ms, expose_view_container, fmt_epoch, getStrokeColor, infer_period, info_traffic, inject_svg_icons, is_current_period, iso_week, local_input_to_epoch, ms_to_epoch, period_bounds, period_bounds_epoch, period_label, period_name, period_shift, period_spec, period_start, refresh_clear, register_c_g6_nodes_tree, register_c_yui_form, register_c_yui_fsm_graph, register_c_yui_gclass, register_c_yui_gobj_tree_js, register_c_yui_json, register_c_yui_json_graph, register_c_yui_map, register_c_yui_nav, register_c_yui_pager, register_c_yui_period, register_c_yui_schema_editor, register_c_yui_service_view, register_c_yui_shell, register_c_yui_treedb_graph, register_c_yui_treedb_schema, register_c_yui_treedb_topic_with_form, register_c_yui_treedb_topics, register_c_yui_uplot, register_c_yui_window, register_c_yui_window_manager, register_c_yui_wizard, removeChildElements, removeClasses, rolling_bounds, safe_locale, set_active_state, set_cancel_state, set_pressed_state, set_submit_state, setup_dev, setup_frontend_view, toggleClasses, yui_asset_element, yui_asset_id, yui_asset_ids, yui_asset_missing, yui_asset_src, yui_is_dark, yui_mount_service_view, yui_shell_close_drawer, yui_shell_close_dropdown, yui_shell_confirm_danger, yui_shell_confirm_ok, yui_shell_confirm_yesno, yui_shell_confirm_yesnocancel, yui_shell_navigate, yui_shell_open_drawer, yui_shell_overlay_dismissed, yui_shell_pop_escape, yui_shell_push_escape, yui_shell_refresh_avatars, yui_shell_register_event_handler, yui_shell_register_overlay, yui_shell_set_avatar_provider, yui_shell_set_connection_state, yui_shell_set_sub_routes, yui_shell_set_toolbar_item_badge, yui_shell_set_toolbar_item_icon, yui_shell_set_translator, yui_shell_show_error, yui_shell_show_info, yui_shell_show_modal, yui_shell_show_route_map, yui_shell_show_warning, yui_shell_toggle_drawer, yui_shell_unpark_route, yui_theme_now, yui_toolbar, yui_watch_theme };
69779
+ export { EditControl, MarkerControl, YUI_FILE_ACCEPT, YUI_PERIODS, YUI_PERIODS_DEFAULT, YUI_ROLLING, addClasses, apply_dev_traces, attach_clear, build_dev_panel, dev_window_was_open, disableElements, enableElements, epoch_to_local_input, epoch_to_ms, expose_view_container, fmt_epoch, getStrokeColor, infer_period, info_traffic, inject_svg_icons, is_current_period, iso_week, local_input_to_epoch, ms_to_epoch, period_bounds, period_bounds_epoch, period_label, period_name, period_shift, period_spec, period_start, refresh_clear, register_c_g6_nodes_tree, register_c_yui_form, register_c_yui_fsm_graph, register_c_yui_gclass, register_c_yui_gobj_tree_js, register_c_yui_json, register_c_yui_json_graph, register_c_yui_map, register_c_yui_nav, register_c_yui_pager, register_c_yui_period, register_c_yui_schema_editor, register_c_yui_service_view, register_c_yui_shell, register_c_yui_treedb_graph, register_c_yui_treedb_schema, register_c_yui_treedb_topic_with_form, register_c_yui_treedb_topics, register_c_yui_uplot, register_c_yui_window, register_c_yui_window_manager, register_c_yui_wizard, removeChildElements, removeClasses, rolling_bounds, safe_locale, set_active_state, set_cancel_state, set_pressed_state, set_submit_state, setup_dev, setup_frontend_view, toggleClasses, yui_array_buffer_to_base64, yui_asset_element, yui_asset_id, yui_asset_ids, yui_asset_missing, yui_asset_src, yui_file_control, yui_file_id_label, yui_file_read, yui_file_sha256, yui_file_size_label, yui_files_manifest, yui_is_dark, yui_mount_service_view, yui_shell_close_drawer, yui_shell_close_dropdown, yui_shell_confirm_danger, yui_shell_confirm_ok, yui_shell_confirm_yesno, yui_shell_confirm_yesnocancel, yui_shell_navigate, yui_shell_open_drawer, yui_shell_overlay_dismissed, yui_shell_pop_escape, yui_shell_push_escape, yui_shell_refresh_avatars, yui_shell_register_event_handler, yui_shell_register_overlay, yui_shell_set_avatar_provider, yui_shell_set_connection_state, yui_shell_set_sub_routes, yui_shell_set_toolbar_item_badge, yui_shell_set_toolbar_item_icon, yui_shell_set_translator, yui_shell_show_error, yui_shell_show_info, yui_shell_show_modal, yui_shell_show_route_map, yui_shell_show_warning, yui_shell_toggle_drawer, yui_shell_unpark_route, yui_theme_now, yui_toolbar, yui_watch_theme };
69303
69780
 
69304
69781
  //# sourceMappingURL=gobj-ui.es.js.map