@yuneta/gobj-ui 2.2.4 → 2.3.0

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.
@@ -36,13 +36,13 @@ let i18next = require("i18next");
36
36
  i18next = __toESM(i18next, 1);
37
37
  let tom_select = require("tom-select");
38
38
  tom_select = __toESM(tom_select, 1);
39
+ let vanilla_jsoneditor = require("vanilla-jsoneditor");
39
40
  let tabulator_tables = require("tabulator-tables");
40
41
  let maplibre_gl = require("maplibre-gl");
41
42
  maplibre_gl = __toESM(maplibre_gl, 1);
42
43
  let uplot = require("uplot");
43
44
  uplot = __toESM(uplot, 1);
44
45
  let _antv_g6 = require("@antv/g6");
45
- let vanilla_jsoneditor = require("vanilla-jsoneditor");
46
46
  //#region src/yui_dev.js
47
47
  /***********************************************************************
48
48
  * ui_dev.js
@@ -5399,6 +5399,10 @@ var attrs_table$11 = [
5399
5399
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "template", 0, "<div>You must supply a template for form</div>", "Template for form, object -> TEMPLATE, array -> DESC"),
5400
5400
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
5401
5401
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
5402
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
5403
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
5404
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
5405
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
5402
5406
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
5403
5407
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable with 'click' (BAD with editable=true)"),
5404
5408
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
@@ -5520,34 +5524,35 @@ function build_ui$9(gobj) {
5520
5524
  let $container = (0, _yuneta_gobj_js.createElement2)([
5521
5525
  "div",
5522
5526
  {
5523
- class: "C_YUI_FORM is-flex is-flex-direction-row overscroll-contain",
5527
+ class: "C_YUI_FORM is-flex is-flex-direction-column overscroll-contain",
5524
5528
  style: "height:100%; width:100%; box-sizing:border-box;"
5525
5529
  },
5526
5530
  [[
5527
5531
  "div",
5528
5532
  {
5529
5533
  class: "yui-form is-flex-grow-1 p-2",
5530
- style: "overflow-y:auto; height:100%;min-height:0;"
5534
+ style: "overflow-y:auto; min-height:0;"
5531
5535
  },
5532
5536
  build_form(gobj)
5533
5537
  ], [
5534
5538
  "div",
5535
5539
  {
5536
- class: "yui-toolbar-form ml-2 is-flex-shrink-0 is-flex is-flex-direction-column is-justify-content-space-between",
5537
- style: "overflow-y:auto; height:100%; max-width:90px;"
5540
+ class: "yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center",
5541
+ style: "width:100%; flex-wrap:wrap; row-gap:6px;"
5538
5542
  },
5539
5543
  [[
5540
5544
  "div",
5541
5545
  {
5542
- class: "p-1 is-flex is-flex-direction-column is-align-items-center",
5543
- style: "width:100%; row-gap:6px;"
5546
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
5547
+ style: "column-gap:6px;"
5544
5548
  },
5545
5549
  [
5546
5550
  [
5547
5551
  "button",
5548
5552
  {
5549
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center button-save",
5550
- style: "width:100%;",
5553
+ class: "button p-1 button-save",
5554
+ title: "save",
5555
+ "aria-label": "save",
5551
5556
  disabled: true
5552
5557
  },
5553
5558
  [[
@@ -5557,7 +5562,7 @@ function build_ui$9(gobj) {
5557
5562
  ], [
5558
5563
  "span",
5559
5564
  {
5560
- class: "is-hidden-mobile",
5565
+ class: "is-hidden-mobile pl-1 pr-1",
5561
5566
  i18n: "save"
5562
5567
  },
5563
5568
  "save"
@@ -5570,8 +5575,9 @@ function build_ui$9(gobj) {
5570
5575
  [
5571
5576
  "button",
5572
5577
  {
5573
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center button-undo",
5574
- style: "width:100%;",
5578
+ class: "button p-1 button-undo",
5579
+ title: "undo",
5580
+ "aria-label": "undo",
5575
5581
  disabled: true
5576
5582
  },
5577
5583
  [[
@@ -5581,7 +5587,7 @@ function build_ui$9(gobj) {
5581
5587
  ], [
5582
5588
  "span",
5583
5589
  {
5584
- class: "is-hidden-mobile",
5590
+ class: "is-hidden-mobile pl-1 pr-1",
5585
5591
  i18n: "undo"
5586
5592
  },
5587
5593
  "undo"
@@ -5594,8 +5600,9 @@ function build_ui$9(gobj) {
5594
5600
  [
5595
5601
  "button",
5596
5602
  {
5597
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center",
5598
- style: "width:100%;"
5603
+ class: "button p-1",
5604
+ title: "clear",
5605
+ "aria-label": "clear"
5599
5606
  },
5600
5607
  [[
5601
5608
  "span",
@@ -5604,7 +5611,7 @@ function build_ui$9(gobj) {
5604
5611
  ], [
5605
5612
  "span",
5606
5613
  {
5607
- class: "is-hidden-mobile",
5614
+ class: "is-hidden-mobile pl-1 pr-1",
5608
5615
  i18n: "clear"
5609
5616
  },
5610
5617
  "clear"
@@ -5618,14 +5625,15 @@ function build_ui$9(gobj) {
5618
5625
  ], [
5619
5626
  "div",
5620
5627
  {
5621
- class: "p-1 is-flex is-flex-direction-column is-align-items-center",
5622
- style: "width:100%; row-gap:6px;"
5628
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
5629
+ style: "column-gap:6px;"
5623
5630
  },
5624
5631
  [[
5625
5632
  "button",
5626
5633
  {
5627
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center",
5628
- style: "width:100%;"
5634
+ class: "button p-1",
5635
+ title: "copy",
5636
+ "aria-label": "copy"
5629
5637
  },
5630
5638
  [[
5631
5639
  "span",
@@ -5634,7 +5642,7 @@ function build_ui$9(gobj) {
5634
5642
  ], [
5635
5643
  "span",
5636
5644
  {
5637
- class: "is-hidden-mobile",
5645
+ class: "is-hidden-mobile pl-1 pr-1",
5638
5646
  i18n: "copy"
5639
5647
  },
5640
5648
  "copy"
@@ -5646,8 +5654,9 @@ function build_ui$9(gobj) {
5646
5654
  ], [
5647
5655
  "button",
5648
5656
  {
5649
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center",
5650
- style: "width:100%;"
5657
+ class: "button p-1",
5658
+ title: "paste",
5659
+ "aria-label": "paste"
5651
5660
  },
5652
5661
  [[
5653
5662
  "span",
@@ -5656,7 +5665,7 @@ function build_ui$9(gobj) {
5656
5665
  ], [
5657
5666
  "span",
5658
5667
  {
5659
- class: "is-hidden-mobile",
5668
+ class: "is-hidden-mobile pl-1 pr-1",
5660
5669
  i18n: "paste"
5661
5670
  },
5662
5671
  "paste"
@@ -5670,6 +5679,7 @@ function build_ui$9(gobj) {
5670
5679
  ]]
5671
5680
  ]);
5672
5681
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", $container);
5682
+ apply_form_mode(gobj, $container.querySelector("form"));
5673
5683
  (0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
5674
5684
  let $parent = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$parent");
5675
5685
  if ($parent) $parent.appendChild($container);
@@ -5712,7 +5722,11 @@ function build_html_form(gobj, $form, prefix, template) {
5712
5722
  const field_desc = (0, _yuneta_gobj_js.template_get_field_desc)(key, value);
5713
5723
  let html_field_conf = build_form_field_conf(gobj, field_desc);
5714
5724
  html_field_conf.name = field_desc.name;
5715
- html_field_conf.label = field_desc.header;
5725
+ let topic_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name");
5726
+ let label_keys = topic_name ? [topic_name + "." + field_desc.name, field_desc.name] : [field_desc.name];
5727
+ const NO_I18N = "\0";
5728
+ html_field_conf.label = (0, i18next.t)(label_keys, { defaultValue: field_desc.header });
5729
+ html_field_conf.label_i18n = (0, i18next.t)(field_desc.name, { defaultValue: NO_I18N }) !== NO_I18N ? field_desc.name : null;
5716
5730
  html_field_conf.placeholder = field_desc.placeholder ? (0, i18next.t)(field_desc.placeholder) : "";
5717
5731
  let placeholders = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "placeholders");
5718
5732
  if (placeholders[key]) html_field_conf.placeholder = placeholders[key];
@@ -5756,7 +5770,7 @@ function build_form_field_conf(gobj, field_desc) {
5756
5770
  switch (field_desc.type) {
5757
5771
  case "object":
5758
5772
  case "dict":
5759
- field_conf.tag = "fieldset";
5773
+ field_conf.tag = "jsoneditor";
5760
5774
  break;
5761
5775
  case "template":
5762
5776
  field_conf.tag = "fieldset";
@@ -5771,7 +5785,7 @@ function build_form_field_conf(gobj, field_desc) {
5771
5785
  break;
5772
5786
  case "array":
5773
5787
  case "list":
5774
- field_conf.tag = "table";
5788
+ field_conf.tag = "jsoneditor";
5775
5789
  break;
5776
5790
  case "enum":
5777
5791
  switch (field_desc.real_type) {
@@ -5789,7 +5803,10 @@ function build_form_field_conf(gobj, field_desc) {
5789
5803
  }
5790
5804
  break;
5791
5805
  case "hook": break;
5792
- case "fkey": break;
5806
+ case "fkey":
5807
+ field_conf.tag = "select2";
5808
+ field_conf.options = get_fkey_select_options(gobj, field_desc);
5809
+ break;
5793
5810
  case "image":
5794
5811
  case "string":
5795
5812
  field_conf.tag = "input";
@@ -5874,6 +5891,112 @@ function build_form_field_conf(gobj, field_desc) {
5874
5891
  }
5875
5892
  return field_conf;
5876
5893
  }
5894
+ /************************************************************
5895
+ * Options for a fkey field: the ids of the parent topic's
5896
+ * rows, taken from the `fkey_options` attr
5897
+ * ({topic_name: [ids or {id} records]}, supplied by the host —
5898
+ * the form never queries the backend or its parent itself).
5899
+ * Read at build time.
5900
+ ************************************************************/
5901
+ function get_fkey_select_options(gobj, field_desc) {
5902
+ let fkey_desc = field_desc.fkey;
5903
+ if (!(0, _yuneta_gobj_js.is_object)(fkey_desc) || Object.keys(fkey_desc).length === 0) {
5904
+ (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: fkey field '%s' without fkey mapping", (0, _yuneta_gobj_js.gobj_short_name)(gobj), field_desc.name));
5905
+ return [];
5906
+ }
5907
+ let topic_name = Object.keys(fkey_desc)[0];
5908
+ let fkey_options = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "fkey_options");
5909
+ let list = (0, _yuneta_gobj_js.is_object)(fkey_options) ? fkey_options[topic_name] : null;
5910
+ if (!(0, _yuneta_gobj_js.is_array)(list)) {
5911
+ (0, _yuneta_gobj_js.log_warning)((0, _yuneta_gobj_js.sprintf)("%s: no fkey_options for topic '%s' (field '%s')", (0, _yuneta_gobj_js.gobj_short_name)(gobj), topic_name, field_desc.name));
5912
+ return [];
5913
+ }
5914
+ let options = [];
5915
+ for (let item of list) {
5916
+ let id = (0, _yuneta_gobj_js.is_object)(item) ? item.id : item;
5917
+ if ((0, _yuneta_gobj_js.is_string)(id) && !(0, _yuneta_gobj_js.empty_string)(id)) options.push(id);
5918
+ }
5919
+ return options;
5920
+ }
5921
+ /************************************************************
5922
+ * Encode the select2 value (array of ids) into fkey ref(s)
5923
+ * "topic_name^id^hook_name", shaped by the column real type:
5924
+ * string -> single ref (or null if no selection)
5925
+ * object/dict -> {ref: true, ...}
5926
+ * array/list -> [ref, ...]
5927
+ ************************************************************/
5928
+ function build_fkey_ref$1(gobj, field_desc, value) {
5929
+ let fkey_desc = field_desc.fkey;
5930
+ if (!(0, _yuneta_gobj_js.is_object)(fkey_desc) || Object.keys(fkey_desc).length === 0) {
5931
+ (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: fkey field '%s' without fkey mapping", (0, _yuneta_gobj_js.gobj_short_name)(gobj), field_desc.name));
5932
+ return null;
5933
+ }
5934
+ let topic_name = Object.keys(fkey_desc)[0];
5935
+ let hook_name = fkey_desc[topic_name];
5936
+ let refs;
5937
+ switch (field_desc.real_type) {
5938
+ case "string":
5939
+ refs = null;
5940
+ break;
5941
+ case "object":
5942
+ case "dict":
5943
+ refs = {};
5944
+ break;
5945
+ case "array":
5946
+ case "list":
5947
+ refs = [];
5948
+ break;
5949
+ default:
5950
+ (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: fkey field '%s' with bad real_type '%s'", (0, _yuneta_gobj_js.gobj_short_name)(gobj), field_desc.name, field_desc.real_type));
5951
+ return null;
5952
+ }
5953
+ if (!(0, _yuneta_gobj_js.is_array)(value)) value = value ? [value] : [];
5954
+ for (let v of value) {
5955
+ let id = (0, _yuneta_gobj_js.is_object)(v) ? v.id : v;
5956
+ if (!(0, _yuneta_gobj_js.is_string)(id) || (0, _yuneta_gobj_js.empty_string)(id)) {
5957
+ (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: fkey field '%s' with bad value: %s", (0, _yuneta_gobj_js.gobj_short_name)(gobj), field_desc.name, JSON.stringify(v)));
5958
+ continue;
5959
+ }
5960
+ let ref = topic_name + "^" + id + "^" + hook_name;
5961
+ switch (field_desc.real_type) {
5962
+ case "string": return ref;
5963
+ case "object":
5964
+ case "dict":
5965
+ refs[ref] = true;
5966
+ break;
5967
+ case "array":
5968
+ case "list":
5969
+ refs.push(ref);
5970
+ break;
5971
+ }
5972
+ }
5973
+ return refs;
5974
+ }
5975
+ /************************************************************
5976
+ * Apply the form mode to the pkey field:
5977
+ * "update" -> pkey readonly, not required
5978
+ * "create" -> pkey writable and required (readonly if rowid)
5979
+ * Empty form_mode keeps the template-declared behaviour
5980
+ * (backward compatible).
5981
+ ************************************************************/
5982
+ function apply_form_mode(gobj, $form) {
5983
+ let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "form_mode");
5984
+ if ((0, _yuneta_gobj_js.empty_string)(mode)) return;
5985
+ let pkey = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "pkey");
5986
+ let $input = $form.querySelector(`[name="${pkey}"]`);
5987
+ if (!$input) {
5988
+ (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: form_mode '%s' but form has no pkey field '%s'", (0, _yuneta_gobj_js.gobj_short_name)(gobj), mode, pkey));
5989
+ return;
5990
+ }
5991
+ let is_rowid = $input.field_desc && $input.field_desc.type === "rowid";
5992
+ if (mode === "create" && !is_rowid) {
5993
+ $input.removeAttribute("readonly");
5994
+ $input.setAttribute("required", "");
5995
+ } else {
5996
+ $input.setAttribute("readonly", "readonly");
5997
+ $input.removeAttribute("required");
5998
+ }
5999
+ }
5877
6000
  /******************************************************************
5878
6001
  * With html field conf builds an html element,
5879
6002
  * ready to add to the form
@@ -5904,7 +6027,7 @@ function build_form_field_conf(gobj, field_desc) {
5904
6027
  * default_value:
5905
6028
  * }
5906
6029
  ******************************************************************/
5907
- function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, name, label, placeholder, options, extras, readonly, required, hidden, default_value }) {
6030
+ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, name, label, label_i18n, placeholder, options, extras, readonly, required, hidden, default_value }) {
5908
6031
  let $element;
5909
6032
  let kw_add_table_row = {
5910
6033
  $row: null,
@@ -5926,7 +6049,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
5926
6049
  "label",
5927
6050
  {
5928
6051
  class: "label ",
5929
- i18n: label,
6052
+ i18n: label_i18n || label,
5930
6053
  for: name
5931
6054
  },
5932
6055
  label
@@ -5960,7 +6083,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
5960
6083
  "label",
5961
6084
  {
5962
6085
  class: "label ",
5963
- i18n: label,
6086
+ i18n: label_i18n || label,
5964
6087
  for: name
5965
6088
  },
5966
6089
  label
@@ -6002,7 +6125,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6002
6125
  "label",
6003
6126
  {
6004
6127
  class: "label ",
6005
- i18n: label,
6128
+ i18n: label_i18n || label,
6006
6129
  for: name
6007
6130
  },
6008
6131
  label
@@ -6033,7 +6156,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6033
6156
  "label",
6034
6157
  {
6035
6158
  class: "label ",
6036
- i18n: label,
6159
+ i18n: label_i18n || label,
6037
6160
  for: name
6038
6161
  },
6039
6162
  label
@@ -6064,7 +6187,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6064
6187
  "label",
6065
6188
  {
6066
6189
  class: "label",
6067
- i18n: label
6190
+ i18n: label_i18n || label
6068
6191
  },
6069
6192
  label
6070
6193
  ]]
@@ -6148,7 +6271,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6148
6271
  "label",
6149
6272
  {
6150
6273
  class: "label ",
6151
- i18n: label,
6274
+ i18n: label_i18n || label,
6152
6275
  for: name
6153
6276
  },
6154
6277
  label
@@ -6174,7 +6297,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6174
6297
  switch (tag) {
6175
6298
  case "input": {
6176
6299
  let attrs = {
6177
- class: `input ${name === "id" ? "with-focus" : ""}`,
6300
+ class: `input ${name === (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "pkey") ? "with-focus" : ""}`,
6178
6301
  name,
6179
6302
  type: inputType,
6180
6303
  placeholder
@@ -6227,8 +6350,8 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6227
6350
  $control.appendChild($extend);
6228
6351
  break;
6229
6352
  }
6230
- case "select":
6231
- $extend = (0, _yuneta_gobj_js.createElement2)([
6353
+ case "select": {
6354
+ let $wrapper = (0, _yuneta_gobj_js.createElement2)([
6232
6355
  "div",
6233
6356
  { class: "select" },
6234
6357
  [
@@ -6241,11 +6364,16 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6241
6364
  i18n: option
6242
6365
  },
6243
6366
  option
6244
- ])
6367
+ ]),
6368
+ { "change": function(evt) {
6369
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_RECORD_CHANGED", {}, gobj);
6370
+ } }
6245
6371
  ]
6246
6372
  ]);
6247
- $control.appendChild($extend);
6373
+ $control.appendChild($wrapper);
6374
+ $extend = $wrapper.querySelector("select");
6248
6375
  break;
6376
+ }
6249
6377
  case "select2":
6250
6378
  $extend = (0, _yuneta_gobj_js.createElement2)([
6251
6379
  "select",
@@ -6267,6 +6395,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6267
6395
  $extend.tom_select = new tom_select.default($extend, {
6268
6396
  allowEmptyOption: true,
6269
6397
  placeholder,
6398
+ maxItems: field_desc.type === "fkey" && field_desc.real_type === "string" ? 1 : null,
6270
6399
  plugins: {
6271
6400
  "clear_button": { "title": (0, i18next.t)("Remove all selected options") },
6272
6401
  "no_active_items": {},
@@ -6278,17 +6407,26 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6278
6407
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_RECORD_CHANGED", {}, gobj);
6279
6408
  });
6280
6409
  break;
6281
- case "checkbox":
6282
- $extend = (0, _yuneta_gobj_js.createElement2)([
6410
+ case "checkbox": {
6411
+ let $wrapper = (0, _yuneta_gobj_js.createElement2)([
6283
6412
  "label",
6284
6413
  { class: "checkbox" },
6285
- ["input", {
6286
- type: "checkbox",
6287
- name
6288
- }]
6414
+ [
6415
+ "input",
6416
+ {
6417
+ type: "checkbox",
6418
+ name
6419
+ },
6420
+ "",
6421
+ { "change": function(evt) {
6422
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_RECORD_CHANGED", {}, gobj);
6423
+ } }
6424
+ ]
6289
6425
  ]);
6290
- $control.appendChild($extend);
6426
+ $control.appendChild($wrapper);
6427
+ $extend = $wrapper.querySelector("input");
6291
6428
  break;
6429
+ }
6292
6430
  case "radio":
6293
6431
  $extend = (0, _yuneta_gobj_js.createOneHtml)(`
6294
6432
  <label class="radio">
@@ -6301,6 +6439,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6301
6439
  case "jsoneditor": {
6302
6440
  let attrs = {
6303
6441
  class: "jsoneditor jse-theme-dark",
6442
+ name,
6304
6443
  style: ""
6305
6444
  };
6306
6445
  Object.assign(attrs, extras);
@@ -6308,6 +6447,18 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6308
6447
  else if (required) attrs.required = "";
6309
6448
  $extend = (0, _yuneta_gobj_js.createElement2)(["div", attrs]);
6310
6449
  $control.appendChild($extend);
6450
+ $extend.jsoneditor = new vanilla_jsoneditor.JSONEditor({
6451
+ target: $extend,
6452
+ props: {
6453
+ readOnly: !!readonly,
6454
+ onChange: function() {
6455
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_RECORD_CHANGED", {}, gobj);
6456
+ },
6457
+ timestampTag: function({ field, value, path }) {
6458
+ return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
6459
+ }
6460
+ }
6461
+ });
6311
6462
  break;
6312
6463
  }
6313
6464
  case "fieldset": {
@@ -6703,7 +6854,7 @@ function template2columns(gobj, columns, template, sub_elements) {
6703
6854
  /************************************************************
6704
6855
  *
6705
6856
  ************************************************************/
6706
- function validate_form$1(gobj, $form) {
6857
+ function validate_form(gobj, $form) {
6707
6858
  let validated = true;
6708
6859
  if (!$form.checkValidity()) validated = false;
6709
6860
  $form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
@@ -6724,7 +6875,7 @@ function getInputValue($input) {
6724
6875
  /************************************************************
6725
6876
  * Function to get input value based on its type
6726
6877
  ************************************************************/
6727
- function get_form_values$1(gobj, $form) {
6878
+ function get_form_values(gobj, $form) {
6728
6879
  const record = {};
6729
6880
  $form.querySelectorAll(".yui-form-data-input").forEach(($input) => {
6730
6881
  $input.closest(".xfield");
@@ -6805,9 +6956,10 @@ function get_form_values$1(gobj, $form) {
6805
6956
  /************************************************************
6806
6957
  *
6807
6958
  ************************************************************/
6808
- function clear_data$1(gobj, $form) {
6959
+ function clear_data(gobj, $form) {
6960
+ let pkey = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "pkey");
6809
6961
  $form.querySelectorAll("input, select, textarea, .jsoneditor").forEach(($input) => {
6810
- if ($input.name !== "id") if ($input.classList.contains("jsoneditor")) {
6962
+ if ($input.name !== pkey) if ($input.classList.contains("jsoneditor")) {
6811
6963
  let jsoneditor = $input.jsoneditor;
6812
6964
  if (jsoneditor) jsoneditor.set({ text: "" });
6813
6965
  } else if ($input.type === "checkbox") $input.checked = false;
@@ -6826,7 +6978,7 @@ function clear_data$1(gobj, $form) {
6826
6978
  /************************************************************
6827
6979
  *
6828
6980
  ************************************************************/
6829
- function clear_validation$1(gobj, $form) {
6981
+ function clear_validation(gobj, $form) {
6830
6982
  $form.querySelectorAll("input").forEach(($input) => {
6831
6983
  $input.classList.remove("is-danger");
6832
6984
  });
@@ -6864,10 +7016,10 @@ function convertColor(color) {
6864
7016
  /************************************************************
6865
7017
  *
6866
7018
  ************************************************************/
6867
- function set_form_values$1(gobj, template, $form, record) {
7019
+ function set_form_values(gobj, template, $form, record) {
6868
7020
  let modified = false;
6869
- clear_data$1(gobj, $form);
6870
- if (!record || record.length === 0) record = (0, _yuneta_gobj_js.create_template_record)(template);
7021
+ clear_data(gobj, $form);
7022
+ if (!record || Object.keys(record).length === 0) record = (0, _yuneta_gobj_js.create_template_record)(template);
6871
7023
  $form.querySelectorAll(".yui-form-data-input").forEach(($input) => {
6872
7024
  let field_desc = $input.field_desc;
6873
7025
  let name = $input.dataset.treedb_name;
@@ -6883,10 +7035,16 @@ function set_form_values$1(gobj, template, $form, record) {
6883
7035
  modified = true;
6884
7036
  }
6885
7037
  value = treedb_value_2_form_value(gobj, field_desc, value);
7038
+ if (value === void 0) value = null;
6886
7039
  switch (tag) {
6887
7040
  case "input":
6888
7041
  switch (input_type) {
6889
- case "datetime-local": break;
7042
+ case "datetime-local":
7043
+ if (value instanceof Date && !isNaN(value.getTime())) {
7044
+ let pad = (n) => String(n).padStart(2, "0");
7045
+ $input.value = `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
7046
+ } else $input.value = "";
7047
+ break;
6890
7048
  case "color":
6891
7049
  $input.value = convertColor(value);
6892
7050
  break;
@@ -6913,11 +7071,11 @@ function set_form_values$1(gobj, template, $form, record) {
6913
7071
  break;
6914
7072
  case "jsoneditor":
6915
7073
  let jsoneditor = $input.jsoneditor;
6916
- if (jsoneditor) jsoneditor.set(value);
7074
+ if (jsoneditor) jsoneditor.set(value ? value : { text: "" });
6917
7075
  break;
6918
7076
  case "fieldset": break;
6919
7077
  case "table":
6920
- load_tabulator_data(gobj, $input, value);
7078
+ load_tabulator_data(gobj, $input, value ? value : []);
6921
7079
  break;
6922
7080
  case "coordinates":
6923
7081
  if ($input.tagName !== "INPUT") $input = $input.querySelector("input");
@@ -6963,7 +7121,22 @@ function treedb_value_2_form_value(gobj, field_desc, value) {
6963
7121
  let type = field_desc.type;
6964
7122
  field_desc.real_type;
6965
7123
  switch (type) {
6966
- case "fkey":
7124
+ case "fkey": {
7125
+ let new_value = [];
7126
+ if ((0, _yuneta_gobj_js.is_string)(value) && !(0, _yuneta_gobj_js.empty_string)(value)) {
7127
+ let fkey = (0, _yuneta_gobj_js.treedb_decoder_fkey)(field_desc, value);
7128
+ if (fkey) new_value.push(fkey.id);
7129
+ } else if ((0, _yuneta_gobj_js.is_array)(value)) for (let v of value) {
7130
+ let fkey = (0, _yuneta_gobj_js.treedb_decoder_fkey)(field_desc, v);
7131
+ if (fkey) new_value.push(fkey.id);
7132
+ }
7133
+ else if ((0, _yuneta_gobj_js.is_object)(value)) for (let v of Object.keys(value)) {
7134
+ let fkey = (0, _yuneta_gobj_js.treedb_decoder_fkey)(field_desc, v);
7135
+ if (fkey) new_value.push(fkey.id);
7136
+ }
7137
+ value = new_value;
7138
+ break;
7139
+ }
6967
7140
  case "hook":
6968
7141
  case "enum":
6969
7142
  case "uuid":
@@ -6973,8 +7146,7 @@ function treedb_value_2_form_value(gobj, field_desc, value) {
6973
7146
  case "url": break;
6974
7147
  case "now":
6975
7148
  case "time":
6976
- value = parseInt(value) || 0;
6977
- value = new Date(value);
7149
+ value = /* @__PURE__ */ new Date((parseInt(value) || 0) * 1e3);
6978
7150
  break;
6979
7151
  case "date":
6980
7152
  case "color":
@@ -6990,13 +7162,19 @@ function treedb_value_2_form_value(gobj, field_desc, value) {
6990
7162
  case "base64": break;
6991
7163
  case "string":
6992
7164
  case "integer":
7165
+ case "real":
7166
+ case "boolean": break;
6993
7167
  case "object":
6994
7168
  case "dict":
7169
+ value = { json: (0, _yuneta_gobj_js.is_object)(value) ? value : {} };
7170
+ break;
7171
+ case "blob":
7172
+ value = { json: (0, _yuneta_gobj_js.is_object)(value) || (0, _yuneta_gobj_js.is_array)(value) ? value : {} };
7173
+ break;
6995
7174
  case "array":
6996
7175
  case "list":
6997
- case "real":
6998
- case "boolean": break;
6999
- case "blob": break;
7176
+ value = { json: (0, _yuneta_gobj_js.is_array)(value) ? value : [] };
7177
+ break;
7000
7178
  case "number": break;
7001
7179
  case "coordinates":
7002
7180
  let longitude = 0;
@@ -7028,7 +7206,9 @@ function form_value_2_treedb_value(gobj, field_desc, value) {
7028
7206
  let type = field_desc.type;
7029
7207
  let real_type = field_desc.real_type;
7030
7208
  switch (type) {
7031
- case "fkey": break;
7209
+ case "fkey":
7210
+ value = build_fkey_ref$1(gobj, field_desc, value);
7211
+ break;
7032
7212
  case "hook":
7033
7213
  value = null;
7034
7214
  break;
@@ -7136,7 +7316,9 @@ function ac_load_record(gobj, event, kw, src) {
7136
7316
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "record", kw);
7137
7317
  let $form = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container").querySelector("form");
7138
7318
  (0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", false);
7139
- set_changed_stated(gobj, set_form_values$1(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, kw));
7319
+ let modified = set_form_values(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, kw);
7320
+ apply_form_mode(gobj, $form);
7321
+ set_changed_stated(gobj, modified);
7140
7322
  (0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", true);
7141
7323
  return 0;
7142
7324
  }
@@ -7146,8 +7328,8 @@ function ac_load_record(gobj, event, kw, src) {
7146
7328
  ************************************************************/
7147
7329
  function ac_save_record(gobj, event, kw, src) {
7148
7330
  let $form = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container").querySelector("form");
7149
- if (validate_form$1(gobj, $form)) {
7150
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_SAVE_RECORD", get_form_values$1(gobj, $form));
7331
+ if (validate_form(gobj, $form)) {
7332
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_SAVE_RECORD", get_form_values(gobj, $form));
7151
7333
  set_changed_stated(gobj, false);
7152
7334
  } else {
7153
7335
  kw.abort_close = true;
@@ -7161,7 +7343,7 @@ function ac_save_record(gobj, event, kw, src) {
7161
7343
  function ac_undo_record(gobj, event, kw, src) {
7162
7344
  let $form = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container").querySelector("form");
7163
7345
  (0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", false);
7164
- set_form_values$1(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "record"));
7346
+ set_form_values(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "record"));
7165
7347
  set_changed_stated(gobj, false);
7166
7348
  (0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", true);
7167
7349
  return 0;
@@ -7171,8 +7353,8 @@ function ac_undo_record(gobj, event, kw, src) {
7171
7353
  ************************************************************/
7172
7354
  function ac_clear_record(gobj, event, kw, src) {
7173
7355
  let $form = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container").querySelector("form");
7174
- clear_validation$1(gobj, $form);
7175
- clear_data$1(gobj, $form);
7356
+ clear_validation(gobj, $form);
7357
+ clear_data(gobj, $form);
7176
7358
  return 0;
7177
7359
  }
7178
7360
  /************************************************************
@@ -7217,8 +7399,8 @@ function ac_record_changed(gobj, event, kw, src) {
7217
7399
  ************************************************************/
7218
7400
  function ac_copy_record(gobj, event, kw, src) {
7219
7401
  let $form = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container").querySelector("form");
7220
- if (validate_form$1(gobj, $form)) {
7221
- let data = { record: get_form_values$1(gobj, $form) };
7402
+ if (validate_form(gobj, $form)) {
7403
+ let data = { record: get_form_values(gobj, $form) };
7222
7404
  navigator.clipboard.writeText(JSON.stringify(data));
7223
7405
  }
7224
7406
  return 0;
@@ -7231,7 +7413,7 @@ function ac_paste_record(gobj, event, kw, src) {
7231
7413
  let record = kw.record;
7232
7414
  if ((0, _yuneta_gobj_js.is_object)(record)) {
7233
7415
  (0, _yuneta_gobj_js.gobj_write_bool_attr)(gobj, "initialized", true);
7234
- set_form_values$1(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, record);
7416
+ set_form_values(gobj, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "template"), $form, record);
7235
7417
  }
7236
7418
  return 0;
7237
7419
  }
@@ -7377,6 +7559,7 @@ function create_gclass$11(gclass_name) {
7377
7559
  * Register GClass
7378
7560
  ***************************************************************/
7379
7561
  function register_c_yui_form() {
7562
+ if ((0, _yuneta_gobj_js.gclass_find_by_name)(GCLASS_NAME$11, false)) return 0;
7380
7563
  return create_gclass$11(GCLASS_NAME$11);
7381
7564
  }
7382
7565
  //#endregion
@@ -8454,7 +8637,7 @@ function onClick(gobj, e) {
8454
8637
  const map = gobj.priv.xmap;
8455
8638
  const coordinates = e.features[0].geometry.coordinates.slice();
8456
8639
  const properties = e.features[0].properties;
8457
- const gobj_service = (0, _yuneta_gobj_js.gobj_find_service)(properties.gobj_service_name, true);
8640
+ const gobj_service = properties.gobj_service_name ? (0, _yuneta_gobj_js.gobj_find_service)(properties.gobj_service_name, false) : null;
8458
8641
  if (gobj_service) {
8459
8642
  let window_service_name = `window-map-${(0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj_service))}`;
8460
8643
  let popupContent = (0, _yuneta_gobj_js.createElement2)([
@@ -11748,6 +11931,93 @@ function resolve_route(item_index, route) {
11748
11931
  };
11749
11932
  }
11750
11933
  //#endregion
11934
+ //#region src/shell_section_index.js
11935
+ /***********************************************************************
11936
+ * shell_section_index.js
11937
+ *
11938
+ * Pure helper for C_YUI_SHELL: synthesize the stage target of a
11939
+ * section-index landing route.
11940
+ *
11941
+ * A level-1 menu item that declares `submenu.index` opts out of
11942
+ * the redirect-to-default-child behaviour: its own route becomes
11943
+ * a real resting, deep-linkable route whose view is the submenu
11944
+ * itself rendered as a "cards" C_YUI_NAV (the shell's view
11945
+ * contract only requires a $container by the end of mt_create,
11946
+ * which C_YUI_NAV already satisfies). Config:
11947
+ *
11948
+ * "submenu": {
11949
+ * "render": { "top-sub": "tabs" },
11950
+ * "index": true — landing in stage "main"
11951
+ * "index": {"stage": "x"} — landing in stage "x"
11952
+ * }
11953
+ *
11954
+ * Precedence: an explicit inline `target` on the item wins (this
11955
+ * helper returns null then). When the target is synthesized,
11956
+ * `submenu.default` becomes inert for the section: navigate_to()
11957
+ * only redirects to the default child while the entry has NO
11958
+ * target.
11959
+ *
11960
+ * Tabs and cards never coexist (DRY of navigation): while the
11961
+ * index is on stage the shell collapses the secondary zone, and
11962
+ * for index sections the tab strip defaults to desktop/tablet
11963
+ * only — on mobile a "backbar" nav (← <section>) takes its place
11964
+ * inside a child view. See secondary_nav_renders().
11965
+ *
11966
+ * Split out of c_yui_shell.js so it can be unit-tested without
11967
+ * a DOM.
11968
+ *
11969
+ * Copyright (c) 2026, ArtGins.
11970
+ * All Rights Reserved.
11971
+ ***********************************************************************/
11972
+ /************************************************************
11973
+ * Return the synthesized target for `item`'s section route,
11974
+ * or null when the item doesn't opt in.
11975
+ ************************************************************/
11976
+ function section_index_target(menu_id, item) {
11977
+ if (!item || !item.route || item.target) return null;
11978
+ let sub = item.submenu;
11979
+ if (!sub || !Array.isArray(sub.items) || !sub.index) return null;
11980
+ return {
11981
+ stage: (typeof sub.index === "object" ? sub.index : {}).stage || "main",
11982
+ gclass: "C_YUI_NAV",
11983
+ kw: {
11984
+ menu_id: `index.${menu_id}.${item.id}`,
11985
+ nav_label: item.name || item.id || "",
11986
+ menu_items: sub.items,
11987
+ layout: "cards",
11988
+ level: "secondary"
11989
+ }
11990
+ };
11991
+ }
11992
+ /************************************************************
11993
+ * Return the list of render configs for one (submenu, zone)
11994
+ * pair. `layout` is the raw submenu.render[zone] value
11995
+ * (string or object).
11996
+ *
11997
+ * Without submenu.index: the declared render, unchanged.
11998
+ * With submenu.index (list → detail):
11999
+ * - the declared nav defaults to show_on ">=tablet" (an
12000
+ * explicit show_on in the render object wins);
12001
+ * - a "backbar" nav (show_on "<tablet") is appended so a
12002
+ * child view offers "← <section>" on mobile instead of
12003
+ * the duplicated tab strip. Disable with
12004
+ * index: {backbar: false}; tune its breakpoints with
12005
+ * index: {backbar: {show_on: "..."}}.
12006
+ ************************************************************/
12007
+ function secondary_nav_renders(item, layout) {
12008
+ let cfg = layout && typeof layout === "object" ? Object.assign({}, layout) : { layout: String(layout || "vertical") };
12009
+ let sub = item && item.submenu || {};
12010
+ if (!sub.index) return [cfg];
12011
+ let index_cfg = typeof sub.index === "object" ? sub.index : {};
12012
+ if (!cfg.show_on) cfg.show_on = ">=tablet";
12013
+ if (index_cfg.backbar === false) return [cfg];
12014
+ return [cfg, {
12015
+ layout: "backbar",
12016
+ show_on: (typeof index_cfg.backbar === "object" && index_cfg.backbar ? index_cfg.backbar : {}).show_on || "<tablet",
12017
+ back_route: item && item.route || ""
12018
+ }];
12019
+ }
12020
+ //#endregion
11751
12021
  //#region src/c_yui_shell.js
11752
12022
  /***********************************************************************
11753
12023
  * c_yui_shell.js
@@ -11767,6 +12037,11 @@ function resolve_route(item_index, route) {
11767
12037
  * $container in its stage, hide the previous one. Lifecycle per item
11768
12038
  * decides when the gobj is created/destroyed.
11769
12039
  *
12040
+ * A primary item with `submenu.index` gets a section-index landing:
12041
+ * its own route mounts the submenu as a "cards" C_YUI_NAV in the
12042
+ * stage instead of redirecting to the default child (see
12043
+ * shell_section_index.js for the contract).
12044
+ *
11770
12045
  * Hash-based 2-level routing (#/primary/secondary). No dependency on
11771
12046
  * C_YUI_ROUTING.
11772
12047
  *
@@ -12142,12 +12417,13 @@ function build_item_index(gobj, config) {
12142
12417
  let items = menu && menu.items || [];
12143
12418
  for (let item of items) {
12144
12419
  if (item.route) {
12420
+ let target = item.target || section_index_target(menu_id, item);
12145
12421
  let prev = priv.item_index[item.route];
12146
- if (!prev || !prev.target && item.target) priv.item_index[item.route] = {
12422
+ if (!prev || !prev.target && target) priv.item_index[item.route] = {
12147
12423
  item,
12148
12424
  parent_item: null,
12149
- stage: item.target && item.target.stage || null,
12150
- target: item.target || null,
12425
+ stage: target && target.stage || null,
12426
+ target,
12151
12427
  menu_id
12152
12428
  };
12153
12429
  }
@@ -12225,11 +12501,15 @@ function instantiate_menus(gobj, config) {
12225
12501
  (0, _yuneta_gobj_js.log_warning)(`C_YUI_SHELL: submenu of '${menu_id}.${item.id}' renders in unknown zone '${zone_id}'`);
12226
12502
  continue;
12227
12503
  }
12228
- let $c = (0, _yuneta_gobj_js.gobj_read_attr)(instantiate_nav_in_zone(gobj, {
12229
- items: sub.items,
12230
- render: { [zone_id]: render_to_obj(layout) }
12231
- }, `secondary.${menu_id}.${item.id}`, zone_id, "secondary", item.name || ""), "$container");
12232
- if ($c) $c.classList.add("is-hidden");
12504
+ let renders = secondary_nav_renders(item, render_to_obj(layout));
12505
+ let sub_menu_id = `secondary.${menu_id}.${item.id}`;
12506
+ for (let render_cfg of renders) {
12507
+ let $c = (0, _yuneta_gobj_js.gobj_read_attr)(instantiate_nav_in_zone(gobj, {
12508
+ items: sub.items,
12509
+ render: { [zone_id]: render_cfg }
12510
+ }, sub_menu_id, zone_id, "secondary", item.name || ""), "$container");
12511
+ if ($c) $c.classList.add("is-hidden");
12512
+ }
12233
12513
  }
12234
12514
  }
12235
12515
  }
@@ -12243,9 +12523,8 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
12243
12523
  let priv = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "priv");
12244
12524
  let render_cfg = menu.render && (menu.render[zone_id] || menu.render["*"]) || { layout: "vertical" };
12245
12525
  if ((0, _yuneta_gobj_js.is_string)(render_cfg)) render_cfg = { layout: render_cfg };
12246
- let nav_name = `nav_${menu_id.replace(/\./g, "_")}_${zone_id}`;
12247
12526
  let layout = render_cfg.layout || "vertical";
12248
- let nav = (0, _yuneta_gobj_js.gobj_create)(nav_name, "C_YUI_NAV", {
12527
+ let nav = (0, _yuneta_gobj_js.gobj_create)(`nav_${menu_id.replace(/\./g, "_")}_${zone_id}` + (layout === "backbar" ? "_backbar" : ""), "C_YUI_NAV", {
12249
12528
  menu_id,
12250
12529
  nav_label: nav_label || "",
12251
12530
  menu_items: menu.items || [],
@@ -12254,6 +12533,8 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
12254
12533
  icon_pos: render_cfg.icon_pos || default_icon_pos(zone_id),
12255
12534
  show_label: render_cfg.show_label !== false,
12256
12535
  level,
12536
+ show_on: render_cfg.show_on || "",
12537
+ back_route: render_cfg.back_route || "",
12257
12538
  shell: gobj
12258
12539
  }, gobj);
12259
12540
  let $c = (0, _yuneta_gobj_js.gobj_read_attr)(nav, "$container");
@@ -12438,6 +12719,8 @@ function build_view_gobj(gobj, entry, route, stage) {
12438
12719
  }
12439
12720
  stage.el.appendChild($view);
12440
12721
  (0, _yuneta_gobj_js.gobj_start)(view);
12722
+ let priv = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "priv");
12723
+ if (priv && typeof priv.translator === "function" && target.gclass === "C_YUI_NAV") (0, _yuneta_gobj_js.refresh_language)($view, priv.translator);
12441
12724
  return view;
12442
12725
  }
12443
12726
  function safe_id(s) {
@@ -13022,7 +13305,7 @@ function update_secondary_nav_visibility(gobj, entry) {
13022
13305
  let active_primary_id = entry.parent_item && entry.parent_item.id || entry.item && entry.item.id || "";
13023
13306
  let target_secondary_id = `secondary.${entry.menu_id || ""}.${active_primary_id}`;
13024
13307
  let active_primary = entry.parent_item || entry.item || null;
13025
- let has_secondary = !!(active_primary && active_primary.submenu && Array.isArray(active_primary.submenu.items) && active_primary.submenu.items.some((it) => it && it.route));
13308
+ let has_secondary = !!!(!entry.parent_item && active_primary && active_primary.submenu && active_primary.submenu.index) && !!(active_primary && active_primary.submenu && Array.isArray(active_primary.submenu.items) && active_primary.submenu.items.some((it) => it && it.route));
13026
13309
  let config = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "config") || {};
13027
13310
  let zones_cfg = config.shell && config.shell.zones || {};
13028
13311
  for (let z in zones_cfg) if (zones_cfg[z] && zones_cfg[z].host === "menu.secondary") {
@@ -13035,7 +13318,7 @@ function update_secondary_nav_visibility(gobj, entry) {
13035
13318
  if (!nav_menu_id.startsWith("secondary.")) continue;
13036
13319
  let $c = (0, _yuneta_gobj_js.gobj_read_attr)(nav, "$container");
13037
13320
  if (!$c) continue;
13038
- if (nav_menu_id === target_secondary_id) $c.classList.remove("is-hidden");
13321
+ if (nav_menu_id === target_secondary_id && has_secondary) $c.classList.remove("is-hidden");
13039
13322
  else $c.classList.add("is-hidden");
13040
13323
  }
13041
13324
  }
@@ -13220,6 +13503,86 @@ function yui_shell_close_dropdown(shell_gobj) {
13220
13503
  close_toolbar_dropdown(shell_gobj);
13221
13504
  }
13222
13505
  //#endregion
13506
+ //#region src/nav_cards_helpers.js
13507
+ /***********************************************************************
13508
+ * nav_cards_helpers.js
13509
+ *
13510
+ * Pure descriptor builders for C_YUI_NAV's "cards" layout — a
13511
+ * grid of tappable cards, one per item, used as the section-index
13512
+ * landing of a submenu (list → detail pattern). Split out of
13513
+ * c_yui_nav.js so it can be unit-tested without a DOM: the
13514
+ * functions return createElement2 node descriptors, never
13515
+ * HTMLElements.
13516
+ *
13517
+ * Copyright (c) 2026, ArtGins.
13518
+ * All Rights Reserved.
13519
+ ***********************************************************************/
13520
+ /************************************************************
13521
+ * Descriptor of one card: an <a> carrying the same data-*
13522
+ * contract as every other nav item (data-route/data-item-id/
13523
+ * data-disabled), so C_YUI_NAV's delegated click handler and
13524
+ * active-route highlight work unchanged.
13525
+ ************************************************************/
13526
+ function card_descriptor(it, show_label) {
13527
+ let children = [];
13528
+ let label = it.name || "";
13529
+ if (it.icon) children.push([
13530
+ "span",
13531
+ { class: "icon is-medium" },
13532
+ ["i", {
13533
+ class: it.icon,
13534
+ "aria-hidden": "true"
13535
+ }]
13536
+ ]);
13537
+ if (show_label && label) children.push([
13538
+ "span",
13539
+ {
13540
+ class: "yui-nav-label",
13541
+ i18n: label
13542
+ },
13543
+ label
13544
+ ]);
13545
+ let a_attrs = {
13546
+ class: "yui-nav-item yui-nav-card",
13547
+ href: it.route ? "#" + it.route : "#",
13548
+ "data-item-id": it.id,
13549
+ "data-route": it.route || "",
13550
+ "data-disabled": it.disabled ? "1" : "0",
13551
+ "aria-label": label || it.id
13552
+ };
13553
+ if (label) a_attrs["data-i18n-aria-label"] = label;
13554
+ let tip = it.tooltip || it.aria_label;
13555
+ if (tip) {
13556
+ a_attrs.title = tip;
13557
+ a_attrs["data-i18n-title"] = tip;
13558
+ }
13559
+ if (it.disabled) {
13560
+ a_attrs["aria-disabled"] = "true";
13561
+ a_attrs["tabindex"] = "-1";
13562
+ }
13563
+ return [
13564
+ "a",
13565
+ a_attrs,
13566
+ children
13567
+ ];
13568
+ }
13569
+ /************************************************************
13570
+ * Descriptor of the whole grid. Decorative items (`header`/
13571
+ * `divider`) are dropped, same policy as the tabs layout.
13572
+ ************************************************************/
13573
+ function cards_grid_descriptor(items, show_label) {
13574
+ let cards = [];
13575
+ for (let it of items || []) {
13576
+ if (!it || it.type === "header" || it.type === "divider") continue;
13577
+ cards.push(card_descriptor(it, show_label));
13578
+ }
13579
+ return [
13580
+ "div",
13581
+ {},
13582
+ cards
13583
+ ];
13584
+ }
13585
+ //#endregion
13223
13586
  //#region src/c_yui_nav.js
13224
13587
  /***********************************************************************
13225
13588
  * c_yui_nav.js
@@ -13235,6 +13598,17 @@ function yui_shell_close_dropdown(shell_gobj) {
13235
13598
  * "drawer" — off-canvas vertical (toggled by hamburger)
13236
13599
  * "submenu" — Bulma .menu nested under a heading
13237
13600
  * "accordion" — Bulma .menu list with collapsible group
13601
+ * "cards" — grid of tappable cards (section-index landing;
13602
+ * mounted as a stage view by C_YUI_SHELL when a
13603
+ * primary item declares submenu.index)
13604
+ * "backbar" — single "← <section>" link back to the section
13605
+ * index; the mobile replacement of the tab strip
13606
+ * for submenu.index sections (the shell collapses
13607
+ * the secondary zone while the index is on stage)
13608
+ *
13609
+ * The `show_on` attr (same expressions as the shell's zone
13610
+ * `show_on`) applies breakpoint visibility classes to the nav
13611
+ * container; build_ui re-applies them on every rebuild.
13238
13612
  *
13239
13613
  * Each item supports:
13240
13614
  * id, name, icon (CSS class or svg id), route, badge, disabled
@@ -13272,7 +13646,9 @@ var SUPPORTED_LAYOUTS = [
13272
13646
  "tabs",
13273
13647
  "drawer",
13274
13648
  "submenu",
13275
- "accordion"
13649
+ "accordion",
13650
+ "cards",
13651
+ "backbar"
13276
13652
  ];
13277
13653
  function is_decorative(it) {
13278
13654
  return !!(it && (it.type === "header" || it.type === "divider"));
@@ -13290,6 +13666,8 @@ var attrs_table$4 = [
13290
13666
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "icon_pos", 0, "left", "left|right|top|bottom"),
13291
13667
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "show_label", 0, true, "Show item label next to/under the icon"),
13292
13668
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
13669
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
13670
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
13293
13671
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
13294
13672
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
13295
13673
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
@@ -13373,11 +13751,23 @@ function build_ui$4(gobj) {
13373
13751
  case "accordion":
13374
13752
  $container = render_accordion(gobj, items);
13375
13753
  break;
13754
+ case "cards":
13755
+ $container = render_cards(gobj, items);
13756
+ break;
13757
+ case "backbar":
13758
+ $container = render_backbar(gobj);
13759
+ break;
13376
13760
  default: $container = render_vertical(gobj, items);
13377
13761
  }
13378
13762
  $container.setAttribute("data-nav-zone", zone);
13379
13763
  $container.setAttribute("data-nav-layout", layout);
13380
13764
  $container.classList.add("C_YUI_NAV", "yui-nav", `yui-nav-${layout}`);
13765
+ let show_on = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "show_on") || "";
13766
+ if (!(0, _yuneta_gobj_js.empty_string)(show_on) && show_on !== "*") {
13767
+ let visible = breakpoints_from_expr(show_on);
13768
+ for (let bp of BULMA_BP_ORDER) if (!visible[bp]) $container.classList.add(bulma_hidden_class(bp));
13769
+ $container.setAttribute("data-show-on", show_on);
13770
+ }
13381
13771
  if (layout !== "drawer") {
13382
13772
  $container.setAttribute("role", "navigation");
13383
13773
  if (!$container.hasAttribute("aria-label")) {
@@ -13548,6 +13938,51 @@ function render_tabs(gobj, items) {
13548
13938
  ]
13549
13939
  ]);
13550
13940
  }
13941
+ function render_cards(gobj, items) {
13942
+ return (0, _yuneta_gobj_js.createElement2)(cards_grid_descriptor(items, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "show_label")));
13943
+ }
13944
+ /************************************************************
13945
+ * "backbar": a single "← <section>" link back to the section
13946
+ * index route. Static DOM — the active child view names itself
13947
+ * in its own content, so the bar only carries the way back plus
13948
+ * section context. While the index itself is on stage the shell
13949
+ * collapses the whole secondary zone (tabs and cards never
13950
+ * coexist), so the bar needs no self-hiding logic.
13951
+ ************************************************************/
13952
+ function render_backbar(gobj) {
13953
+ let back_route = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "back_route") || "";
13954
+ let label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "";
13955
+ let label_attrs = { class: "yui-nav-label" };
13956
+ if (label) label_attrs.i18n = label;
13957
+ let a_attrs = {
13958
+ class: "yui-nav-item yui-nav-back",
13959
+ href: back_route ? "#" + back_route : "#",
13960
+ "data-item-id": "__index__",
13961
+ "data-route": back_route,
13962
+ "aria-label": label || "back"
13963
+ };
13964
+ if (label) a_attrs["data-i18n-aria-label"] = label;
13965
+ return (0, _yuneta_gobj_js.createElement2)([
13966
+ "div",
13967
+ {},
13968
+ [
13969
+ "a",
13970
+ a_attrs,
13971
+ [[
13972
+ "span",
13973
+ { class: "icon is-small" },
13974
+ ["i", {
13975
+ class: "yi-arrow-left",
13976
+ "aria-hidden": "true"
13977
+ }]
13978
+ ], [
13979
+ "span",
13980
+ label_attrs,
13981
+ label
13982
+ ]]
13983
+ ]
13984
+ ]);
13985
+ }
13551
13986
  function render_drawer(gobj, items) {
13552
13987
  let menu_id = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "menu_id") || "drawer";
13553
13988
  let wrap = document.createElement("div");
@@ -13849,6 +14284,7 @@ function ac_route_changed(gobj, event, kw, src) {
13849
14284
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "active_route", route);
13850
14285
  let $c = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
13851
14286
  if (!$c) return 0;
14287
+ if ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "layout") === "backbar") return 0;
13852
14288
  $c.querySelectorAll(".is-active").forEach((n) => n.classList.remove("is-active"));
13853
14289
  let id_to_mark = null;
13854
14290
  if (level === "primary") {
@@ -14818,7 +15254,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
14818
15254
  * record
14819
15255
  * }
14820
15256
  ********************************************/
14821
- function ac_create_record$1(gobj, event, kw, src) {
15257
+ function ac_create_record(gobj, event, kw, src) {
14822
15258
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
14823
15259
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
14824
15260
  let record = kw.record;
@@ -14835,7 +15271,7 @@ function ac_create_record$1(gobj, event, kw, src) {
14835
15271
  * record
14836
15272
  * }
14837
15273
  ********************************************/
14838
- function ac_update_record$1(gobj, event, kw, src) {
15274
+ function ac_update_record(gobj, event, kw, src) {
14839
15275
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
14840
15276
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
14841
15277
  let record = kw.record;
@@ -14908,12 +15344,12 @@ function create_gclass$3(gclass_name) {
14908
15344
  ],
14909
15345
  [
14910
15346
  "EV_CREATE_RECORD",
14911
- ac_create_record$1,
15347
+ ac_create_record,
14912
15348
  null
14913
15349
  ],
14914
15350
  [
14915
15351
  "EV_UPDATE_RECORD",
14916
- ac_update_record$1,
15352
+ ac_update_record,
14917
15353
  null
14918
15354
  ],
14919
15355
  [
@@ -15012,7 +15448,6 @@ var attrs_table$2 = [
15012
15448
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "tabulator", 0, null, "Tabulator instance"),
15013
15449
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "table_id", 0, null, "Table div ID"),
15014
15450
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "toolbar_id", 0, null, "Toolbar ID"),
15015
- (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "form_id", 0, null, "Edit form ID"),
15016
15451
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "popup_id", 0, null, "Edit form popup ID"),
15017
15452
  (0, _yuneta_gobj_js.SDATA_END)()
15018
15453
  ];
@@ -15020,7 +15455,9 @@ var PRIVATE_DATA$2 = {
15020
15455
  $container: null,
15021
15456
  treedb_name: "",
15022
15457
  topic_name: "",
15023
- tabulator: null
15458
+ tabulator: null,
15459
+ form: null,
15460
+ $popup: null
15024
15461
  };
15025
15462
  var __gclass__$2 = null;
15026
15463
  function popup_mount_layer() {
@@ -15036,7 +15473,6 @@ function mt_create$2(gobj) {
15036
15473
  let name = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
15037
15474
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "table_id", "table" + name);
15038
15475
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "toolbar_id", "toolbar" + name);
15039
- (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "form_id", "form" + name);
15040
15476
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "popup_id", "popup" + name);
15041
15477
  build_ui$2(gobj);
15042
15478
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
@@ -15053,6 +15489,7 @@ function mt_start$2(gobj) {
15053
15489
  * Framework Method: Stop
15054
15490
  ***************************************************************/
15055
15491
  function mt_stop$2(gobj) {
15492
+ close_form_dialog(gobj);
15056
15493
  table__destroy(gobj);
15057
15494
  }
15058
15495
  /***************************************************************
@@ -15392,6 +15829,29 @@ function col_label(col, topic_name) {
15392
15829
  return (0, i18next.t)(topic_name ? [topic_name + "." + col.id, col.id] : [col.id], { defaultValue: col.header || col.id });
15393
15830
  }
15394
15831
  /******************************************************************
15832
+ * Column-title formatter: render the header as a span carrying
15833
+ * `data-i18n` (the shared col id) so a live language switch —
15834
+ * which calls refresh_language(document.body) — retranslates the
15835
+ * Tabulator headers in place, without rebuilding the table. Only
15836
+ * tag translatable columns; otherwise keep the schema header
15837
+ * (refresh_language would otherwise overwrite it with the raw id).
15838
+ * Tabulator re-runs this on redraw, so the full cascade re-applies.
15839
+ ******************************************************************/
15840
+ function col_title_formatter(gobj, col) {
15841
+ let topic_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name");
15842
+ return function(cell, formatterParams, onRendered) {
15843
+ let text = col_label(col, topic_name);
15844
+ let attrs = {};
15845
+ const NO_I18N = "\0";
15846
+ if ((0, i18next.t)(col.id, { defaultValue: NO_I18N }) !== NO_I18N) attrs.i18n = col.id;
15847
+ return (0, _yuneta_gobj_js.createElement2)([
15848
+ "span",
15849
+ attrs,
15850
+ text
15851
+ ]);
15852
+ };
15853
+ }
15854
+ /******************************************************************
15395
15855
  * Build table with Tabulator
15396
15856
  * This fn is called on start and when desc attribute is set.
15397
15857
  * desc contains the description (columns) of table to create
@@ -15496,6 +15956,7 @@ function create_tabulator(gobj) {
15496
15956
  }
15497
15957
  let colDef = {
15498
15958
  title: col_label(col, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name")),
15959
+ titleFormatter: col_title_formatter(gobj, col),
15499
15960
  field: col.id,
15500
15961
  sorter,
15501
15962
  hozAlign,
@@ -15548,7 +16009,13 @@ function create_tabulator(gobj) {
15548
16009
  selectableRows: selectable,
15549
16010
  footerElement: "<span class='yui-tabulator-rowcount' style='display:block;text-align:right;font-size:0.8rem;color:#6b7280;padding:0 0.6rem;'></span>"
15550
16011
  });
15551
- let tabulator = new tabulator_tables.TabulatorFull(`#${table_id}`, tabulator_settings);
16012
+ let $view_container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
16013
+ let $table_el = $view_container ? $view_container.querySelector(`#${table_id}`) : null;
16014
+ if (!$table_el) {
16015
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: table element '#${table_id}' not found in $container`);
16016
+ return;
16017
+ }
16018
+ let tabulator = new tabulator_tables.TabulatorFull($table_el, tabulator_settings);
15552
16019
  function update_rowcount() {
15553
16020
  let $rc = tabulator.element && tabulator.element.querySelector(".yui-tabulator-rowcount");
15554
16021
  if (!$rc) return;
@@ -15683,436 +16150,62 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
15683
16150
  }
15684
16151
  return value;
15685
16152
  }
15686
- /******************************************************************
15687
- * Build the topic form with bulma and select2
15688
- ******************************************************************/
15689
- function build_topic_form(gobj) {
16153
+ /************************************************************
16154
+ * Build the form template from desc.cols: only user-editable
16155
+ * fields reach the form — writable cols, fkeys (linkable) and
16156
+ * the pkey (the hosted C_YUI_FORM's form_mode drives the
16157
+ * pkey's readonly/required state).
16158
+ ************************************************************/
16159
+ function build_form_template(gobj) {
15690
16160
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
15691
- let $form = (0, _yuneta_gobj_js.createElement2)(["form", {
15692
- id: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "form_id"),
15693
- class: "box p-3",
15694
- novalidate: ""
15695
- }]);
15696
- for (let i = 0; i < desc.cols.length; i++) {
15697
- let col = desc.cols[i];
15698
- if (col.id.charAt(0) === "_") continue;
15699
- let [tag, form_input_type, options, extras] = transform__treedb_type_2_form_type(gobj, col);
15700
- if (!tag) continue;
15701
- let id = col.id;
15702
- let flag = col.flag;
15703
- let is_writable = (0, _yuneta_gobj_js.str_in_list)(flag, "writable");
15704
- let is_notnull = (0, _yuneta_gobj_js.str_in_list)(flag, "notnull");
15705
- let is_rowid = (0, _yuneta_gobj_js.str_in_list)(flag, "rowid");
15706
- let is_required = (0, _yuneta_gobj_js.str_in_list)(flag, "required");
15707
- let is_hidden = (0, _yuneta_gobj_js.str_in_list)(flag, "hidden");
15708
- (0, _yuneta_gobj_js.str_in_list)(flag, "persistent");
15709
- (0, _yuneta_gobj_js.str_in_list)(flag, "hook");
15710
- let is_fkey = (0, _yuneta_gobj_js.str_in_list)(flag, "fkey");
15711
- if (id === desc.pkey) is_writable = false;
15712
- if (is_fkey) is_writable = true;
15713
- if (!is_writable) {
15714
- if (id !== desc.pkey) continue;
15715
- }
15716
- if (is_hidden) continue;
15717
- if (is_rowid) {
15718
- is_writable = false;
15719
- is_required = false;
15720
- is_notnull = false;
15721
- }
15722
- let field_conf = {
15723
- tag,
15724
- inputType: form_input_type,
15725
- name: col.id,
15726
- label: col_label(col, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name")),
15727
- placeholder: "",
15728
- options,
15729
- extras,
15730
- readonly: !is_writable,
15731
- required: is_required,
15732
- hidden: is_hidden,
15733
- not_null: is_notnull,
15734
- is_pkey: id === desc.pkey,
15735
- is_rowid
15736
- };
15737
- let $field = create_html_field(field_conf);
15738
- $field.field_conf = field_conf;
15739
- $form.appendChild($field);
16161
+ let template = [];
16162
+ for (let col of desc.cols) {
16163
+ if (!col.id || col.id.charAt(0) === "_") continue;
16164
+ const field_desc = (0, _yuneta_gobj_js.treedb_get_field_desc)(col);
16165
+ if (field_desc.is_hidden) continue;
16166
+ if (field_desc.type === "hook") continue;
16167
+ if (!field_desc.is_writable && field_desc.type !== "fkey" && col.id !== desc.pkey) continue;
16168
+ template.push(col);
15740
16169
  }
15741
- return $form;
16170
+ return template;
15742
16171
  }
15743
16172
  /************************************************************
15744
- * Convert a col type from backend to frontend
15745
- * Return data to build the HTMLElement
15746
- *
15747
- * Options format for select/select2:
15748
- * <option value="reading">Reading</option>
16173
+ * Collect the linkable parent rows for every fkey col:
16174
+ * {topic_name: rows}, asked to the topics manager (parent)
16175
+ * with the same command the old embedded form used. Collected
16176
+ * fresh on every dialog open, so new parent rows are always
16177
+ * offered.
15749
16178
  ************************************************************/
15750
- function transform__treedb_type_2_form_type(gobj, col) {
15751
- const field_desc = (0, _yuneta_gobj_js.treedb_get_field_desc)(col);
15752
- let tag = null;
15753
- let inputType = "";
15754
- let options = [];
15755
- let extras = {};
15756
- switch (field_desc.type) {
15757
- case "image":
15758
- case "string":
15759
- tag = "input";
15760
- inputType = "text";
15761
- break;
15762
- case "integer":
15763
- tag = "input";
15764
- extras.inputmode = "numeric";
15765
- break;
15766
- case "real":
15767
- tag = "input";
15768
- extras.inputmode = "decimal";
15769
- break;
15770
- case "rowid":
15771
- tag = "input";
15772
- inputType = "text";
15773
- break;
15774
- case "boolean":
15775
- tag = "checkbox";
15776
- break;
15777
- case "now":
15778
- case "time":
15779
- tag = "input";
15780
- inputType = "datetime-local";
15781
- switch (field_desc.real_type) {
15782
- case "string": break;
15783
- case "integer": break;
15784
- }
15785
- break;
15786
- case "color":
15787
- tag = "input";
15788
- inputType = "color";
15789
- switch (field_desc.real_type) {
15790
- case "string": break;
15791
- case "integer": break;
15792
- }
15793
- break;
15794
- case "email":
15795
- tag = "input";
15796
- inputType = "email";
15797
- extras.inputmode = "email";
15798
- break;
15799
- case "password":
15800
- tag = "input";
15801
- inputType = "password";
15802
- break;
15803
- case "url":
15804
- tag = "input";
15805
- inputType = "url";
15806
- extras.inputmode = "url";
15807
- break;
15808
- case "tel":
15809
- tag = "input";
15810
- inputType = "tel";
15811
- extras.inputmode = "tel";
15812
- break;
15813
- case "object":
15814
- case "dict":
15815
- case "template":
15816
- case "array":
15817
- case "list":
15818
- case "coordinates":
15819
- case "blob":
15820
- tag = "jsoneditor";
15821
- break;
15822
- case "enum":
15823
- tag = "select2";
15824
- switch (field_desc.real_type) {
15825
- case "string":
15826
- case "object":
15827
- case "dict":
15828
- case "array":
15829
- case "list":
15830
- options = (0, _yuneta_gobj_js.list2options)(field_desc.enum_list, "id", "id");
15831
- break;
15832
- }
15833
- break;
15834
- case "hook": break;
15835
- case "fkey":
15836
- tag = "select2";
15837
- switch (field_desc.real_type) {
15838
- case "string":
15839
- case "object":
15840
- case "dict":
15841
- case "array":
15842
- case "list":
15843
- options = get_fkey_options(gobj, col);
15844
- break;
15845
- }
15846
- break;
15847
- }
15848
- return [
15849
- tag,
15850
- inputType,
15851
- options,
15852
- extras
15853
- ];
15854
- }
15855
- /******************************************************************
15856
- * Helper function to create different form elements
15857
- * Available types:
15858
- * - input
15859
- * - textarea
15860
- * - select
15861
- * - select2
15862
- * - checkbox
15863
- * - radio
15864
- * - jsoneditor
15865
- *
15866
- * {
15867
- * tag: field tag,
15868
- * inputType: input type,
15869
- * name: id
15870
- * label: ""
15871
- * placeholder: "",
15872
- * options: [{id:"", value:""}]]
15873
- * extras: {}
15874
- * readonly: bool
15875
- * required: bool
15876
- * hidden: bool
15877
- * not_null: bool
15878
- * _col: col
15879
- * }
15880
- ******************************************************************/
15881
- function create_html_field({ tag, inputType, name, label, placeholder, options, extras, readonly, required, hidden, not_null }) {
15882
- let base;
15883
- switch (tag) {
15884
- case "input":
15885
- case "textarea":
15886
- case "select":
15887
- case "select2":
15888
- case "checkbox":
15889
- case "radio":
15890
- case "jsoneditor":
15891
- base = [
15892
- "div",
15893
- { class: "xfield field is-horizontal" },
15894
- [[
15895
- "div",
15896
- { class: "field-label is-normal" },
15897
- [[
15898
- "label",
15899
- {
15900
- class: "label ",
15901
- i18n: label,
15902
- for: name
15903
- },
15904
- label
15905
- ]]
15906
- ], [
15907
- "div",
15908
- { class: "field-body" },
15909
- [[
15910
- "div",
15911
- { class: "field" },
15912
- [[
15913
- "div",
15914
- { class: "control" },
15915
- []
15916
- ]]
15917
- ]]
15918
- ]]
15919
- ];
15920
- break;
15921
- default: base = [
15922
- "div",
15923
- { class: "control" },
15924
- "Unsupported element type"
15925
- ];
15926
- }
15927
- let $element = (0, _yuneta_gobj_js.createElement2)(base);
15928
- let $control = $element.querySelector(".control");
15929
- switch (tag) {
15930
- case "input": {
15931
- let attrs = {
15932
- class: `input ${name === "id" ? "with-focus" : ""}`,
15933
- name,
15934
- type: inputType,
15935
- placeholder
15936
- };
15937
- Object.assign(attrs, extras);
15938
- if (readonly) attrs.readonly = "";
15939
- let $extend = (0, _yuneta_gobj_js.createElement2)([
15940
- "input",
15941
- attrs,
15942
- "",
15943
- { "blur": function(evt) {
15944
- validate_field_on_blur(this);
15945
- } }
15946
- ]);
15947
- $control.appendChild($extend);
15948
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
15949
- "p",
15950
- {
15951
- class: "help is-danger",
15952
- style: "display:none"
15953
- },
15954
- ""
15955
- ]));
15956
- break;
15957
- }
15958
- case "jsoneditor": {
15959
- let attrs = {
15960
- class: "jsoneditor jse-theme-dark",
15961
- style: ""
15962
- };
15963
- Object.assign(attrs, extras);
15964
- if (readonly) attrs.readonly = "";
15965
- let $extend = (0, _yuneta_gobj_js.createElement2)(["div", attrs]);
15966
- $extend.setAttribute("name", name);
15967
- $control.appendChild($extend);
15968
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
15969
- "p",
15970
- {
15971
- class: "help is-danger",
15972
- style: "display:none"
15973
- },
15974
- ""
15975
- ]));
15976
- break;
15977
- }
15978
- case "textarea": {
15979
- let attrs = {
15980
- class: "textarea",
15981
- name,
15982
- placeholder
15983
- };
15984
- Object.assign(attrs, extras);
15985
- if (readonly) attrs.readonly = "";
15986
- let $extend = (0, _yuneta_gobj_js.createElement2)([
15987
- "textarea",
15988
- attrs,
15989
- "",
15990
- { "blur": function(evt) {
15991
- validate_field_on_blur(this);
15992
- } }
15993
- ]);
15994
- $control.appendChild($extend);
15995
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
15996
- "p",
15997
- {
15998
- class: "help is-danger",
15999
- style: "display:none"
16000
- },
16001
- ""
16002
- ]));
16003
- break;
16004
- }
16005
- case "select": {
16006
- let $extend = (0, _yuneta_gobj_js.createElement2)([
16007
- "div",
16008
- { class: "select" },
16009
- [
16010
- "select",
16011
- { name },
16012
- options.map((option) => [
16013
- "option",
16014
- {
16015
- value: option.id,
16016
- i18n: option.value
16017
- },
16018
- option.value
16019
- ])
16020
- ]
16021
- ]);
16022
- $control.appendChild($extend);
16023
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
16024
- "p",
16025
- {
16026
- class: "help is-danger",
16027
- style: "display:none"
16028
- },
16029
- ""
16030
- ]));
16031
- break;
16032
- }
16033
- case "select2": {
16034
- let $extend = (0, _yuneta_gobj_js.createElement2)([
16035
- "select",
16036
- {
16037
- name,
16038
- class: "select2-multiple",
16039
- multiple: "multiple",
16040
- style: "width: 100% !important;"
16041
- },
16042
- options.map((option) => [
16043
- "option",
16044
- {
16045
- value: option.id,
16046
- i18n: option.value
16047
- },
16048
- option.value
16049
- ])
16050
- ]);
16051
- $control.appendChild($extend);
16052
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
16053
- "p",
16054
- {
16055
- class: "help is-danger",
16056
- style: "display:none"
16057
- },
16058
- ""
16059
- ]));
16060
- break;
16061
- }
16062
- case "checkbox": {
16063
- let $extend = (0, _yuneta_gobj_js.createElement2)([
16064
- "label",
16065
- { class: "checkbox" },
16066
- ["input", {
16067
- type: "checkbox",
16068
- name
16069
- }]
16070
- ]);
16071
- $control.appendChild($extend);
16072
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
16073
- "p",
16074
- {
16075
- class: "help is-danger",
16076
- style: "display:none"
16077
- },
16078
- ""
16079
- ]));
16080
- break;
16081
- }
16082
- case "radio":
16083
- for (const option of options) {
16084
- let $extend = (0, _yuneta_gobj_js.createOneHtml)(`
16085
- <label class="radio">
16086
- <input type="radio" name="${name}">
16087
- ${(0, i18next.t)(option)}
16088
- </label>
16089
- `);
16090
- $control.appendChild($extend);
16091
- }
16092
- $control.appendChild((0, _yuneta_gobj_js.createElement2)([
16093
- "p",
16094
- {
16095
- class: "help is-danger",
16096
- style: "display:none"
16097
- },
16098
- ""
16099
- ]));
16100
- break;
16101
- default: (0, _yuneta_gobj_js.log_error)(`Unsupported element type, tag ${tag}`);
16179
+ function build_fkey_options(gobj) {
16180
+ let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
16181
+ let fkey_options = {};
16182
+ for (let col of desc.cols) {
16183
+ if ((0, _yuneta_gobj_js.treedb_get_field_desc)(col).type !== "fkey" || !(0, _yuneta_gobj_js.is_object)(col.fkey)) continue;
16184
+ let topic_name = Object.keys(col.fkey)[0];
16185
+ if (!topic_name || fkey_options[topic_name] !== void 0) continue;
16186
+ let webix = (0, _yuneta_gobj_js.gobj_command)((0, _yuneta_gobj_js.gobj_parent)(gobj), "get_topic_data", { topic_name }, gobj);
16187
+ fkey_options[topic_name] = webix && (0, _yuneta_gobj_js.is_array)(webix.data) ? webix.data : [];
16102
16188
  }
16103
- return $element;
16189
+ return fkey_options;
16104
16190
  }
16105
- /******************************************************************
16106
- * Build topic form popup with bulma
16107
- ******************************************************************/
16108
- function build_topic_modal(gobj) {
16109
- const $form = build_topic_form(gobj);
16110
- let title = (0, i18next.t)((0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"));
16191
+ /************************************************************
16192
+ * Open the edit/create dialog hosting a C_YUI_FORM child.
16193
+ * The child is created fresh on every open (schema and fkey
16194
+ * options are read at its build time) and destroyed on close.
16195
+ * Its EV_SAVE_RECORD (CHILD model) arrives already in treedb
16196
+ * shape see ac_form_save_record().
16197
+ ************************************************************/
16198
+ function open_form_dialog(gobj, mode, record) {
16199
+ close_form_dialog(gobj);
16200
+ let priv = gobj.priv;
16201
+ let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
16202
+ let popup_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "popup_id");
16203
+ let topic_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name");
16111
16204
  let $element = (0, _yuneta_gobj_js.createElement2)([
16112
16205
  "div",
16113
16206
  {
16114
- id: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "popup_id"),
16115
- class: "modal"
16207
+ id: popup_id,
16208
+ class: "modal is-active"
16116
16209
  },
16117
16210
  [[
16118
16211
  "div",
@@ -16121,333 +16214,120 @@ function build_topic_modal(gobj) {
16121
16214
  ], [
16122
16215
  "div",
16123
16216
  { class: "modal-card modal-is-responsive" },
16124
- [
16125
- [
16126
- "header",
16127
- { class: "modal-card-head p-3" },
16128
- [[
16129
- "p",
16130
- {
16131
- class: "modal-card-title",
16132
- style: "text-align:center;"
16133
- },
16134
- title
16135
- ], ["button", {
16217
+ [[
16218
+ "header",
16219
+ { class: "modal-card-head p-3" },
16220
+ [[
16221
+ "p",
16222
+ {
16223
+ class: "modal-card-title",
16224
+ style: "text-align:center;"
16225
+ },
16226
+ (0, i18next.t)(topic_name)
16227
+ ], [
16228
+ "button",
16229
+ {
16136
16230
  class: "delete is-large",
16137
16231
  "aria-label": "close"
16138
- }]]
16139
- ],
16140
- [
16141
- "section",
16142
- { class: "modal-card-body p-1" },
16143
- [$form]
16144
- ],
16145
- [
16146
- "footer",
16147
- { class: "modal-card-foot p-3" },
16148
- [[
16149
- "div",
16150
- {
16151
- class: "buttons",
16152
- style: "justify-content:center; width:100%;"
16153
- },
16154
- [
16155
- [
16156
- "button",
16157
- {
16158
- class: "button is-success px-4",
16159
- type: "submit"
16160
- },
16161
- [[
16162
- "span",
16163
- { class: "icon" },
16164
- [["i", { class: "yi-floppy-disk" }]]
16165
- ], [
16166
- "span",
16167
- { i18n: "save" },
16168
- "save"
16169
- ]],
16170
- { click: function(evt) {
16171
- evt.stopPropagation();
16172
- evt.preventDefault();
16173
- let $form = $element.querySelector("form");
16174
- if (validate_form(gobj, $form)) {
16175
- let values = get_form_values($form);
16176
- let ret;
16177
- if ($form.__mode__ === "create") ret = (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CREATE_RECORD", values, gobj);
16178
- else ret = (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_UPDATE_RECORD", values, gobj);
16179
- if (ret === 0) $element.classList.remove("is-active");
16180
- }
16181
- } }
16182
- ],
16183
- [
16184
- "button",
16185
- { class: "button px-4 form-close" },
16186
- [[
16187
- "span",
16188
- { class: "icon" },
16189
- [["i", { class: "yi-xmark" }]]
16190
- ], [
16191
- "span",
16192
- { i18n: "cancel" },
16193
- "cancel"
16194
- ]]
16195
- ],
16196
- [
16197
- "button",
16198
- { class: "button px-4" },
16199
- [[
16200
- "span",
16201
- { class: "icon" },
16202
- [["i", { class: "yi-broom" }]]
16203
- ], [
16204
- "span",
16205
- { i18n: "clear" },
16206
- "clear"
16207
- ]],
16208
- { click: function(evt) {
16209
- evt.stopPropagation();
16210
- clear_validation(gobj, $form);
16211
- clear_data(gobj, $form);
16212
- } }
16213
- ]
16214
- ]
16215
- ]]
16216
- ]
16217
- ]
16232
+ },
16233
+ "",
16234
+ { click: function(evt) {
16235
+ evt.stopPropagation();
16236
+ request_close_form_dialog(gobj);
16237
+ } }
16238
+ ]]
16239
+ ], [
16240
+ "section",
16241
+ {
16242
+ class: "modal-card-body p-2",
16243
+ style: "display:flex; flex-direction:column;"
16244
+ },
16245
+ []
16246
+ ]]
16218
16247
  ]],
16219
16248
  { click: function(evt) {
16220
16249
  evt.stopPropagation();
16221
16250
  } }
16222
16251
  ]);
16223
- $element.querySelectorAll(".modal-close, .form-close, .delete").forEach(function(element) {
16224
- element.addEventListener("click", function(event) {
16225
- event.stopPropagation();
16226
- $element.classList.remove("is-active");
16227
- });
16228
- });
16229
- (0, _yuneta_gobj_js.refresh_language)($element, i18next.t);
16230
- popup_mount_layer().appendChild($element);
16231
- $element.querySelectorAll(".jsoneditor").forEach(function(element) {
16232
- let font_family = "DejaVu Sans Mono, monospace";
16233
- element.style.setProperty("--jse-font-size-mono", "16px");
16234
- element.style.setProperty("--jse-font-family-mono", font_family);
16235
- element.jsoneditor = new vanilla_jsoneditor.JSONEditor({
16236
- target: element,
16237
- props: { timestampTag: function({ field, value, path }) {
16238
- return field === "__t__" || field === "__tm__" || field === "tm" || field === "from_t" || field === "to_t" || field === "t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm" || field === "time";
16239
- } }
16240
- });
16241
- });
16242
- $element.querySelectorAll(".select2-multiple").forEach((el) => {
16243
- if (el.tomselect) return;
16244
- new tom_select.default(el, {
16245
- plugins: ["remove_button"],
16246
- create: false,
16247
- hideSelected: true,
16248
- closeAfterSelect: false
16249
- });
16250
- });
16251
- return $element;
16252
- }
16253
- /************************************************************
16254
- * Show a validation error on a field
16255
- ************************************************************/
16256
- function show_field_error($field, message) {
16257
- $field.querySelectorAll("input, textarea, select").forEach((el) => {
16258
- el.classList.add("is-danger");
16259
- });
16260
- let $help = $field.querySelector(".help");
16261
- if ($help) {
16262
- $help.textContent = message;
16263
- $help.style.display = "block";
16264
- }
16265
- }
16266
- /************************************************************
16267
- * Clear a validation error from a field
16268
- ************************************************************/
16269
- function clear_field_error($field) {
16270
- $field.querySelectorAll("input, textarea, select").forEach((el) => {
16271
- el.classList.remove("is-danger");
16272
- });
16273
- let $help = $field.querySelector(".help");
16274
- if ($help) {
16275
- $help.textContent = "";
16276
- $help.style.display = "none";
16252
+ let $body = $element.querySelector(".modal-card-body");
16253
+ let form = (0, _yuneta_gobj_js.gobj_create_pure_child)("form_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)), "C_YUI_FORM", {
16254
+ template: build_form_template(gobj),
16255
+ record,
16256
+ fkey_options: build_fkey_options(gobj),
16257
+ form_mode: mode,
16258
+ pkey: desc.pkey || "id",
16259
+ topic_name,
16260
+ editable: true,
16261
+ $parent: $body
16262
+ }, gobj);
16263
+ priv.form = form;
16264
+ priv.$popup = $element;
16265
+ let $form_container = (0, _yuneta_gobj_js.gobj_read_attr)(form, "$container");
16266
+ if ($form_container) {
16267
+ $form_container.style.flex = "1 1 auto";
16268
+ $form_container.style.minHeight = "0";
16277
16269
  }
16270
+ popup_mount_layer().appendChild($element);
16271
+ (0, _yuneta_gobj_js.refresh_language)($element, i18next.t);
16272
+ (0, _yuneta_gobj_js.gobj_start)(form);
16273
+ let $with_focus = $element.querySelector(".with-focus");
16274
+ if ($with_focus) $with_focus.focus();
16278
16275
  }
16279
16276
  /************************************************************
16280
- * Get the current value of a field for validation
16277
+ * Close request from the dialog X: if the form carries
16278
+ * unsaved changes ask first (EV_WINDOW_TO_CLOSE contract).
16281
16279
  ************************************************************/
16282
- function get_field_value_for_validation($field, conf) {
16283
- switch (conf.tag) {
16284
- case "input": {
16285
- let $input = $field.querySelector("input");
16286
- if (!$input) return "";
16287
- if ($input.type === "checkbox") return $input.checked;
16288
- return $input.value;
16289
- }
16290
- case "checkbox": {
16291
- let $input = $field.querySelector("input[type=\"checkbox\"]");
16292
- if (!$input) return false;
16293
- return $input.checked;
16294
- }
16295
- case "textarea": {
16296
- let $el = $field.querySelector("textarea");
16297
- if (!$el) return "";
16298
- return $el.value;
16299
- }
16300
- case "select":
16301
- case "select2": {
16302
- let $el = $field.querySelector("select");
16303
- if (!$el) return "";
16304
- if ($el.tomselect) return $el.tomselect.getValue();
16305
- return $el.value;
16306
- }
16307
- case "jsoneditor": {
16308
- let $el = $field.querySelector(".jsoneditor");
16309
- if (!$el || !$el.jsoneditor) return "";
16310
- let val = $el.jsoneditor.get();
16311
- if (val.text !== void 0) return val.text;
16312
- return JSON.stringify(val.json);
16313
- }
16314
- case "radio": {
16315
- let $checked = $field.querySelector("input[type=\"radio\"]:checked");
16316
- if (!$checked) return "";
16317
- return $checked.value;
16280
+ function request_close_form_dialog(gobj) {
16281
+ let priv = gobj.priv;
16282
+ if (priv.form) {
16283
+ let kw = {};
16284
+ (0, _yuneta_gobj_js.gobj_send_event)(priv.form, "EV_WINDOW_TO_CLOSE", kw, gobj);
16285
+ if (kw.abort_close) {
16286
+ get_yesnocancel((0, i18next.t)("All changes will be lost. Are you sure?"), function(evt) {
16287
+ if (evt === "yes") close_form_dialog(gobj);
16288
+ });
16289
+ return;
16318
16290
  }
16319
16291
  }
16320
- return "";
16292
+ close_form_dialog(gobj);
16321
16293
  }
16322
16294
  /************************************************************
16323
- * Check if a field value is empty
16295
+ * Destroy the hosted form and remove the dialog DOM.
16324
16296
  ************************************************************/
16325
- function is_field_value_empty(value) {
16326
- if (value === null || value === void 0 || value === "") return true;
16327
- if (Array.isArray(value) && value.length === 0) return true;
16328
- return false;
16329
- }
16330
- /************************************************************
16331
- * Determine if a field requires validation in the current mode
16332
- ************************************************************/
16333
- function is_field_required(conf, mode) {
16334
- if (conf.is_pkey) return mode === "create" && !conf.is_rowid;
16335
- if (conf.readonly) return false;
16336
- return conf.required || conf.not_null;
16337
- }
16338
- /************************************************************
16339
- * Validate a single field on blur
16340
- * Called from input/textarea blur event handlers
16341
- ************************************************************/
16342
- function validate_field_on_blur(element) {
16343
- let $field = element.closest(".xfield");
16344
- if (!$field || !$field.field_conf) return;
16345
- let conf = $field.field_conf;
16346
- let $form = element.closest("form");
16347
- if (!is_field_required(conf, $form ? $form.__mode__ : "update")) {
16348
- clear_field_error($field);
16349
- return;
16297
+ function close_form_dialog(gobj) {
16298
+ let priv = gobj.priv;
16299
+ if (priv.form) {
16300
+ if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
16301
+ (0, _yuneta_gobj_js.gobj_destroy)(priv.form);
16302
+ priv.form = null;
16350
16303
  }
16351
- if (is_field_value_empty(get_field_value_for_validation($field, conf))) show_field_error($field, (0, i18next.t)("this field is required"));
16352
- else clear_field_error($field);
16353
- }
16354
- /************************************************************
16355
- * Validate all required fields in the form
16356
- * Returns true if valid, false otherwise
16357
- ************************************************************/
16358
- function validate_form(gobj, $form) {
16359
- let is_valid = true;
16360
- let first_invalid = null;
16361
- let mode = $form.__mode__ || "update";
16362
- $form.querySelectorAll(".xfield").forEach(($field) => {
16363
- let conf = $field.field_conf;
16364
- if (!conf) return;
16365
- if (!is_field_required(conf, mode)) {
16366
- clear_field_error($field);
16367
- return;
16368
- }
16369
- if (is_field_value_empty(get_field_value_for_validation($field, conf))) {
16370
- show_field_error($field, (0, i18next.t)("this field is required"));
16371
- is_valid = false;
16372
- if (!first_invalid) first_invalid = $field;
16373
- } else clear_field_error($field);
16374
- });
16375
- if (first_invalid) {
16376
- let $focusable = first_invalid.querySelector("input, select, textarea");
16377
- if ($focusable && $focusable.focus) $focusable.focus();
16304
+ if (priv.$popup) {
16305
+ if (priv.$popup.parentNode) priv.$popup.parentNode.removeChild(priv.$popup);
16306
+ priv.$popup = null;
16378
16307
  }
16379
- return is_valid;
16380
- }
16381
- /************************************************************
16382
- * Clear all validation errors from the form
16383
- ************************************************************/
16384
- function clear_validation(gobj, $form) {
16385
- $form.querySelectorAll(".xfield").forEach(($field) => {
16386
- clear_field_error($field);
16387
- });
16388
16308
  }
16389
16309
  /************************************************************
16390
- * Before show edit/create from,
16391
- * set form state "update" or "create"
16392
- * Only the "id" field is modified,
16393
- * in "update" is readonly
16394
- * in "create" is writable
16395
- * Although by default the pkey (primary key) is "id",
16396
- * use always desc.pkey value, who knows all cases!
16310
+ * True if the topic's pkey col carries the "rowid" flag.
16397
16311
  ************************************************************/
16398
- function set_form_mode(gobj, $form, mode, index) {
16312
+ function pkey_is_rowid(gobj) {
16399
16313
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
16400
- let is_rowid = (0, _yuneta_gobj_js.str_in_list)((0, _yuneta_gobj_js.kwid_find_one_record)(gobj, desc.cols, null, { "id": desc.pkey }).flag, "rowid");
16401
- let $id = $form.querySelector(`[name="${desc.pkey}"]`);
16402
- if ($id) if (mode === "update") {
16403
- $id.setAttribute("readonly", "readonly");
16404
- $id.removeAttribute("required");
16405
- } else if (is_rowid) {
16406
- $id.setAttribute("readonly", "readonly");
16407
- $id.removeAttribute("required");
16408
- } else {
16409
- $id.removeAttribute("readonly");
16410
- $id.setAttribute("required", "");
16411
- }
16412
- else (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: form without id field ('%s')", (0, _yuneta_gobj_js.gobj_short_name)(gobj), desc.pkey));
16413
- if (is_rowid) mode = "create";
16414
- $form.__mode__ = mode;
16415
- $form.__index__ = index;
16314
+ let col_pkey = (0, _yuneta_gobj_js.kwid_find_one_record)(gobj, desc.cols, null, { "id": desc.pkey });
16315
+ if (!col_pkey) return false;
16316
+ return (0, _yuneta_gobj_js.str_in_list)(col_pkey.flag, "rowid");
16416
16317
  }
16417
16318
  /************************************************************
16418
16319
  * Show the edit "update" form of a row (record)
16419
16320
  * internally called from the icon edit at Op column
16420
16321
  ************************************************************/
16421
16322
  function show_edit_form(gobj, row, index) {
16422
- let popup_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "popup_id");
16423
- let form_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "form_id");
16424
- let $element = document.getElementById(popup_id);
16425
- if (!$element) $element = build_topic_modal(gobj);
16426
- let $form = document.getElementById(form_id);
16427
- clear_validation(gobj, $form);
16428
- if ((0, _yuneta_gobj_js.json_size)(row) === 0 || (0, _yuneta_gobj_js.json_size)(row) === 1 && "_check_box_state_" in row) set_form_mode(gobj, $form, "create", index);
16429
- else set_form_mode(gobj, $form, "update", index);
16430
- set_form_values(gobj, $form, row);
16431
- $element.classList.add("is-active");
16432
- let $with_focus = $element.querySelector(".with-focus");
16433
- if ($with_focus) $with_focus.focus();
16323
+ open_form_dialog(gobj, pkey_is_rowid(gobj) ? "create" : "update", row);
16434
16324
  }
16435
16325
  /************************************************************
16436
16326
  * Show the edit "create" form to a new record
16437
16327
  * internally called from the top toolbar +New button
16438
16328
  ************************************************************/
16439
16329
  function show_create_form(gobj, row) {
16440
- let popup_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "popup_id");
16441
- let form_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "form_id");
16442
- let $element = document.getElementById(popup_id);
16443
- if (!$element) $element = build_topic_modal(gobj);
16444
- let $form = document.getElementById(form_id);
16445
- clear_validation(gobj, $form);
16446
- set_form_mode(gobj, $form, "create", -1);
16447
- set_form_values(gobj, $form, row);
16448
- $element.classList.add("is-active");
16449
- let $with_focus = $element.querySelector(".with-focus");
16450
- if ($with_focus) $with_focus.focus();
16330
+ open_form_dialog(gobj, "create", row);
16451
16331
  }
16452
16332
  /************************************************************
16453
16333
  * Return column schema of field id
@@ -16461,174 +16341,6 @@ function get_schema_col(gobj, id) {
16461
16341
  return null;
16462
16342
  }
16463
16343
  /************************************************************
16464
- *
16465
- ************************************************************/
16466
- function clear_data(gobj, $form) {
16467
- $form.querySelectorAll("input, select, textarea, .jsoneditor").forEach((input) => {
16468
- if (input.name !== "id") if (input.classList.contains("jsoneditor")) {
16469
- let jsoneditor = input.jsoneditor;
16470
- if (jsoneditor) jsoneditor.set({ text: "" });
16471
- } else if (input.type === "checkbox") input.checked = false;
16472
- else if (input.type === "radio") input.checked = false;
16473
- else if (input.type === "datetime-local") input.value = null;
16474
- else if (input.multiple) {
16475
- if (input.tomselect) input.tomselect.clear();
16476
- } else input.value = null;
16477
- });
16478
- }
16479
- /************************************************************
16480
- *
16481
- ************************************************************/
16482
- function get_form_values($form) {
16483
- const values = {};
16484
- $form.querySelectorAll("input, select, textarea, .jsoneditor").forEach((input) => {
16485
- if (input.classList.contains("jsoneditor")) {
16486
- let jsoneditor = input.jsoneditor;
16487
- if (jsoneditor) {
16488
- let name = input.getAttribute("name");
16489
- let value = jsoneditor.get();
16490
- if (value.text === void 0) values[name] = JSON.stringify(value.json, null, null);
16491
- else values[name] = value.text;
16492
- }
16493
- } else if (input.type === "checkbox") values[input.name] = input.checked;
16494
- else if (input.type === "radio") {
16495
- if (input.checked) values[input.name] = input.value;
16496
- } else if (input.type === "datetime-local") {
16497
- const date = new Date(input.value);
16498
- const epochTime = Math.floor(date.getTime() / 1e3);
16499
- values[input.name] = epochTime;
16500
- } else if (input.multiple) {
16501
- if (input.tomselect) values[input.name] = input.tomselect.getValue();
16502
- } else values[input.name] = input.value;
16503
- });
16504
- return values;
16505
- }
16506
- /************************************************************
16507
- * Convert a record column value from backend to frontend
16508
- ************************************************************/
16509
- function epochToDateTimeLocal(epochTime) {
16510
- const date = /* @__PURE__ */ new Date(epochTime * 1e3);
16511
- const year = date.getFullYear();
16512
- const month = String(date.getMonth() + 1).padStart(2, "0");
16513
- const day = String(date.getDate()).padStart(2, "0");
16514
- const hours = String(date.getHours()).padStart(2, "0");
16515
- const minutes = String(date.getMinutes()).padStart(2, "0");
16516
- String(date.getSeconds()).padStart(2, "0");
16517
- return `${year}-${month}-${day}T${hours}:${minutes}`;
16518
- }
16519
- /************************************************************
16520
- *
16521
- ************************************************************/
16522
- function set_form_values(gobj, form, values) {
16523
- Object.keys(values).forEach((key) => {
16524
- let element = form.querySelector(`[name="${key}"]`);
16525
- if (element) {
16526
- let value = values[key];
16527
- let col = get_schema_col(gobj, key);
16528
- if (col) value = transform__treedb_value_2_form_value(gobj, col, value);
16529
- switch (element.type) {
16530
- case "select":
16531
- element.value = value;
16532
- break;
16533
- case "select-one":
16534
- element.value = value;
16535
- break;
16536
- case "select-multiple":
16537
- if (element.tomselect) element.tomselect.setValue(value);
16538
- break;
16539
- case "radio":
16540
- let radio = form.querySelector(`input[name="${key}"][value="${values[key]}"]`);
16541
- if (radio) radio.checked = true;
16542
- break;
16543
- case "checkbox":
16544
- element.checked = value;
16545
- break;
16546
- case "text":
16547
- case "textarea":
16548
- case "email":
16549
- element.value = value;
16550
- break;
16551
- case "datetime-local":
16552
- element.value = epochToDateTimeLocal(value);
16553
- break;
16554
- default:
16555
- if (element.classList.contains("jsoneditor")) {
16556
- let jsoneditor = element.jsoneditor;
16557
- if (jsoneditor) jsoneditor.set(value);
16558
- break;
16559
- }
16560
- element.value = value;
16561
- (0, _yuneta_gobj_js.log_error)((0, _yuneta_gobj_js.sprintf)("%s: form type unknown: '%s'", (0, _yuneta_gobj_js.gobj_short_name)(gobj), element.type));
16562
- break;
16563
- }
16564
- }
16565
- });
16566
- }
16567
- /************************************************************
16568
- * Convert a record column value from backend
16569
- * to frontend form field
16570
- ************************************************************/
16571
- function transform__treedb_value_2_form_value(gobj, col, value) {
16572
- const field_desc = (0, _yuneta_gobj_js.treedb_get_field_desc)(col);
16573
- switch (field_desc.type) {
16574
- case "string":
16575
- case "email":
16576
- case "tel":
16577
- case "url": break;
16578
- case "integer": break;
16579
- case "real": break;
16580
- case "boolean": break;
16581
- case "object":
16582
- case "dict":
16583
- case "template":
16584
- case "array":
16585
- case "list":
16586
- case "coordinates":
16587
- case "blob":
16588
- value = { json: value };
16589
- break;
16590
- case "enum":
16591
- switch (field_desc.real_type) {
16592
- case "string": break;
16593
- case "object":
16594
- case "dict":
16595
- case "array":
16596
- case "list": break;
16597
- }
16598
- break;
16599
- case "now":
16600
- case "time":
16601
- switch (field_desc.real_type) {
16602
- case "string":
16603
- case "integer":
16604
- value = parseInt(value) || 0;
16605
- value = new Date(value);
16606
- break;
16607
- }
16608
- break;
16609
- case "color":
16610
- switch (field_desc.real_type) {
16611
- case "string": break;
16612
- case "integer": break;
16613
- }
16614
- break;
16615
- case "hook": break;
16616
- case "fkey":
16617
- let new_value = [];
16618
- if (value) if ((0, _yuneta_gobj_js.is_string)(value)) {
16619
- let fkey = (0, _yuneta_gobj_js.treedb_decoder_fkey)(col, value);
16620
- if (fkey) new_value.push(fkey.id);
16621
- } else if ((0, _yuneta_gobj_js.is_array)(value)) for (let i = 0; i < value.length; i++) {
16622
- let fkey = (0, _yuneta_gobj_js.treedb_decoder_fkey)(col, value[i]);
16623
- if (fkey) new_value.push(fkey.id);
16624
- }
16625
- else (0, _yuneta_gobj_js.log_error)("fkey type unsupported: " + JSON.stringify(value));
16626
- value = new_value;
16627
- break;
16628
- }
16629
- return value;
16630
- }
16631
- /************************************************************
16632
16344
  * Convert from frontend to backend
16633
16345
  * operation: "create" "update"
16634
16346
  ************************************************************/
@@ -16752,18 +16464,6 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation) {
16752
16464
  /************************************************************
16753
16465
  *
16754
16466
  ************************************************************/
16755
- function get_fkey_options(gobj, col) {
16756
- for (let topic_name of Object.keys(col.fkey)) {
16757
- let kw = { topic_name };
16758
- let current_list = (0, _yuneta_gobj_js.gobj_command)((0, _yuneta_gobj_js.gobj_parent)(gobj), "get_topic_data", kw, gobj).data;
16759
- return (0, _yuneta_gobj_js.list2options)(current_list, "id", "id");
16760
- }
16761
- (0, _yuneta_gobj_js.log_error)("No fkey options found" + JSON.stringify(col));
16762
- return null;
16763
- }
16764
- /************************************************************
16765
- *
16766
- ************************************************************/
16767
16467
  function fkey_in_col(col, topic_name, hook_name) {
16768
16468
  if (hook_name === col.fkey[topic_name]) return true;
16769
16469
  return false;
@@ -17093,37 +16793,18 @@ function ac_paste_rows(gobj, event, kw, src) {
17093
16793
  return 0;
17094
16794
  }
17095
16795
  /************************************************************
17096
- * From internal Save button of "create" modal form
17097
- ************************************************************/
17098
- function ac_create_record(gobj, event, kw, src) {
17099
- let new_kw = null;
17100
- try {
17101
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "create");
17102
- } catch (e) {
17103
- (0, _yuneta_gobj_js.log_warning)(e);
17104
- throw e;
17105
- }
17106
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_CREATE_RECORD", {
17107
- topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
17108
- record: new_kw
17109
- });
17110
- return 0;
17111
- }
17112
- /************************************************************
17113
- * From internal Save button of "update" modal form
16796
+ * EV_SAVE_RECORD from the hosted C_YUI_FORM: kw is the
16797
+ * record already in treedb shape (the form itself encodes
16798
+ * fkeys, times and numbers). Route by the form's mode.
17114
16799
  ************************************************************/
17115
- function ac_update_record(gobj, event, kw, src) {
17116
- let new_kw = null;
17117
- try {
17118
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "update");
17119
- } catch (e) {
17120
- (0, _yuneta_gobj_js.log_warning)(e);
17121
- throw e;
17122
- }
17123
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_UPDATE_RECORD", {
16800
+ function ac_form_save_record(gobj, event, kw, src) {
16801
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode") === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
17124
16802
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
17125
- record: new_kw
16803
+ record: kw
17126
16804
  });
16805
+ setTimeout(function() {
16806
+ close_form_dialog(gobj);
16807
+ }, 0);
17127
16808
  return 0;
17128
16809
  }
17129
16810
  /************************************************************
@@ -17253,13 +16934,8 @@ function create_gclass$2(gclass_name) {
17253
16934
  null
17254
16935
  ],
17255
16936
  [
17256
- "EV_CREATE_RECORD",
17257
- ac_create_record,
17258
- null
17259
- ],
17260
- [
17261
- "EV_UPDATE_RECORD",
17262
- ac_update_record,
16937
+ "EV_SAVE_RECORD",
16938
+ ac_form_save_record,
17263
16939
  null
17264
16940
  ],
17265
16941
  [
@@ -17324,6 +17000,7 @@ function create_gclass$2(gclass_name) {
17324
17000
  ["EV_LOAD_NODE_UPDATED", 0],
17325
17001
  ["EV_NODE_DELETED", 0],
17326
17002
  ["EV_EDITION_MODE", 0],
17003
+ ["EV_SAVE_RECORD", 0],
17327
17004
  ["EV_NEW_ROW", 0],
17328
17005
  ["EV_DELETE_ROWS", 0],
17329
17006
  ["EV_COPY_ROWS", 0],
@@ -17347,6 +17024,7 @@ function create_gclass$2(gclass_name) {
17347
17024
  * Register GClass
17348
17025
  ***************************************************************/
17349
17026
  function register_c_yui_treedb_topic_with_form() {
17027
+ if (!(0, _yuneta_gobj_js.gclass_find_by_name)("C_YUI_FORM", false)) register_c_yui_form();
17350
17028
  return create_gclass$2(GCLASS_NAME$2);
17351
17029
  }
17352
17030
  //#endregion