@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.
@@ -1,11 +1,11 @@
1
- import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, duplicate_objects, empty_string, escapeHtml, event_flag_t, gclass_create, gclass_flag_t, getPositionRelativeToBody, gobj_command, gobj_create, gobj_create_service, gobj_current_state, gobj_default_service, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_has_attr, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_pure_number, is_string, json_deep_copy, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_get_ids, kwid_new_dict, list2options, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_json, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
1
+ import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, duplicate_objects, empty_string, escapeHtml, event_flag_t, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_default_service, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_has_attr, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_pure_number, is_string, json_deep_copy, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_get_ids, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_json, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
2
2
  import i18next, { t } from "i18next";
3
3
  import TomSelect from "tom-select";
4
+ import { JSONEditor } from "vanilla-jsoneditor";
4
5
  import { TabulatorFull } from "tabulator-tables";
5
6
  import maplibregl from "maplibre-gl";
6
7
  import uPlot from "uplot";
7
8
  import { BaseLayout, CanvasEvent, Circle, CommonEvent, DragCanvas, EdgeEvent, ExtensionCategory, Graph, HistoryEvent, NodeEvent, Toolbar, register } from "@antv/g6";
8
- import { JSONEditor } from "vanilla-jsoneditor";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __create = Object.create;
11
11
  var __defProp = Object.defineProperty;
@@ -5394,6 +5394,10 @@ var attrs_table$11 = [
5394
5394
  SDATA(data_type_t.DTP_POINTER, "template", 0, "<div>You must supply a template for form</div>", "Template for form, object -> TEMPLATE, array -> DESC"),
5395
5395
  SDATA(data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
5396
5396
  SDATA(data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
5397
+ SDATA(data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
5398
+ SDATA(data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
5399
+ SDATA(data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
5400
+ SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
5397
5401
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
5398
5402
  SDATA(data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable with 'click' (BAD with editable=true)"),
5399
5403
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
@@ -5515,34 +5519,35 @@ function build_ui$9(gobj) {
5515
5519
  let $container = createElement2([
5516
5520
  "div",
5517
5521
  {
5518
- class: "C_YUI_FORM is-flex is-flex-direction-row overscroll-contain",
5522
+ class: "C_YUI_FORM is-flex is-flex-direction-column overscroll-contain",
5519
5523
  style: "height:100%; width:100%; box-sizing:border-box;"
5520
5524
  },
5521
5525
  [[
5522
5526
  "div",
5523
5527
  {
5524
5528
  class: "yui-form is-flex-grow-1 p-2",
5525
- style: "overflow-y:auto; height:100%;min-height:0;"
5529
+ style: "overflow-y:auto; min-height:0;"
5526
5530
  },
5527
5531
  build_form(gobj)
5528
5532
  ], [
5529
5533
  "div",
5530
5534
  {
5531
- class: "yui-toolbar-form ml-2 is-flex-shrink-0 is-flex is-flex-direction-column is-justify-content-space-between",
5532
- style: "overflow-y:auto; height:100%; max-width:90px;"
5535
+ class: "yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center",
5536
+ style: "width:100%; flex-wrap:wrap; row-gap:6px;"
5533
5537
  },
5534
5538
  [[
5535
5539
  "div",
5536
5540
  {
5537
- class: "p-1 is-flex is-flex-direction-column is-align-items-center",
5538
- style: "width:100%; row-gap:6px;"
5541
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
5542
+ style: "column-gap:6px;"
5539
5543
  },
5540
5544
  [
5541
5545
  [
5542
5546
  "button",
5543
5547
  {
5544
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center button-save",
5545
- style: "width:100%;",
5548
+ class: "button p-1 button-save",
5549
+ title: "save",
5550
+ "aria-label": "save",
5546
5551
  disabled: true
5547
5552
  },
5548
5553
  [[
@@ -5552,7 +5557,7 @@ function build_ui$9(gobj) {
5552
5557
  ], [
5553
5558
  "span",
5554
5559
  {
5555
- class: "is-hidden-mobile",
5560
+ class: "is-hidden-mobile pl-1 pr-1",
5556
5561
  i18n: "save"
5557
5562
  },
5558
5563
  "save"
@@ -5565,8 +5570,9 @@ function build_ui$9(gobj) {
5565
5570
  [
5566
5571
  "button",
5567
5572
  {
5568
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center button-undo",
5569
- style: "width:100%;",
5573
+ class: "button p-1 button-undo",
5574
+ title: "undo",
5575
+ "aria-label": "undo",
5570
5576
  disabled: true
5571
5577
  },
5572
5578
  [[
@@ -5576,7 +5582,7 @@ function build_ui$9(gobj) {
5576
5582
  ], [
5577
5583
  "span",
5578
5584
  {
5579
- class: "is-hidden-mobile",
5585
+ class: "is-hidden-mobile pl-1 pr-1",
5580
5586
  i18n: "undo"
5581
5587
  },
5582
5588
  "undo"
@@ -5589,8 +5595,9 @@ function build_ui$9(gobj) {
5589
5595
  [
5590
5596
  "button",
5591
5597
  {
5592
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center",
5593
- style: "width:100%;"
5598
+ class: "button p-1",
5599
+ title: "clear",
5600
+ "aria-label": "clear"
5594
5601
  },
5595
5602
  [[
5596
5603
  "span",
@@ -5599,7 +5606,7 @@ function build_ui$9(gobj) {
5599
5606
  ], [
5600
5607
  "span",
5601
5608
  {
5602
- class: "is-hidden-mobile",
5609
+ class: "is-hidden-mobile pl-1 pr-1",
5603
5610
  i18n: "clear"
5604
5611
  },
5605
5612
  "clear"
@@ -5613,14 +5620,15 @@ function build_ui$9(gobj) {
5613
5620
  ], [
5614
5621
  "div",
5615
5622
  {
5616
- class: "p-1 is-flex is-flex-direction-column is-align-items-center",
5617
- style: "width:100%; row-gap:6px;"
5623
+ class: "p-1 is-flex is-flex-direction-row is-align-items-center",
5624
+ style: "column-gap:6px;"
5618
5625
  },
5619
5626
  [[
5620
5627
  "button",
5621
5628
  {
5622
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center",
5623
- style: "width:100%;"
5629
+ class: "button p-1",
5630
+ title: "copy",
5631
+ "aria-label": "copy"
5624
5632
  },
5625
5633
  [[
5626
5634
  "span",
@@ -5629,7 +5637,7 @@ function build_ui$9(gobj) {
5629
5637
  ], [
5630
5638
  "span",
5631
5639
  {
5632
- class: "is-hidden-mobile",
5640
+ class: "is-hidden-mobile pl-1 pr-1",
5633
5641
  i18n: "copy"
5634
5642
  },
5635
5643
  "copy"
@@ -5641,8 +5649,9 @@ function build_ui$9(gobj) {
5641
5649
  ], [
5642
5650
  "button",
5643
5651
  {
5644
- class: "button p-1 is-flex is-flex-direction-column is-align-items-center",
5645
- style: "width:100%;"
5652
+ class: "button p-1",
5653
+ title: "paste",
5654
+ "aria-label": "paste"
5646
5655
  },
5647
5656
  [[
5648
5657
  "span",
@@ -5651,7 +5660,7 @@ function build_ui$9(gobj) {
5651
5660
  ], [
5652
5661
  "span",
5653
5662
  {
5654
- class: "is-hidden-mobile",
5663
+ class: "is-hidden-mobile pl-1 pr-1",
5655
5664
  i18n: "paste"
5656
5665
  },
5657
5666
  "paste"
@@ -5665,6 +5674,7 @@ function build_ui$9(gobj) {
5665
5674
  ]]
5666
5675
  ]);
5667
5676
  gobj_write_attr(gobj, "$container", $container);
5677
+ apply_form_mode(gobj, $container.querySelector("form"));
5668
5678
  refresh_language($container, t);
5669
5679
  let $parent = gobj_read_attr(gobj, "$parent");
5670
5680
  if ($parent) $parent.appendChild($container);
@@ -5707,7 +5717,11 @@ function build_html_form(gobj, $form, prefix, template) {
5707
5717
  const field_desc = template_get_field_desc(key, value);
5708
5718
  let html_field_conf = build_form_field_conf(gobj, field_desc);
5709
5719
  html_field_conf.name = field_desc.name;
5710
- html_field_conf.label = field_desc.header;
5720
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
5721
+ let label_keys = topic_name ? [topic_name + "." + field_desc.name, field_desc.name] : [field_desc.name];
5722
+ const NO_I18N = "\0";
5723
+ html_field_conf.label = t(label_keys, { defaultValue: field_desc.header });
5724
+ html_field_conf.label_i18n = t(field_desc.name, { defaultValue: NO_I18N }) !== NO_I18N ? field_desc.name : null;
5711
5725
  html_field_conf.placeholder = field_desc.placeholder ? t(field_desc.placeholder) : "";
5712
5726
  let placeholders = gobj_read_attr(gobj, "placeholders");
5713
5727
  if (placeholders[key]) html_field_conf.placeholder = placeholders[key];
@@ -5751,7 +5765,7 @@ function build_form_field_conf(gobj, field_desc) {
5751
5765
  switch (field_desc.type) {
5752
5766
  case "object":
5753
5767
  case "dict":
5754
- field_conf.tag = "fieldset";
5768
+ field_conf.tag = "jsoneditor";
5755
5769
  break;
5756
5770
  case "template":
5757
5771
  field_conf.tag = "fieldset";
@@ -5766,7 +5780,7 @@ function build_form_field_conf(gobj, field_desc) {
5766
5780
  break;
5767
5781
  case "array":
5768
5782
  case "list":
5769
- field_conf.tag = "table";
5783
+ field_conf.tag = "jsoneditor";
5770
5784
  break;
5771
5785
  case "enum":
5772
5786
  switch (field_desc.real_type) {
@@ -5784,7 +5798,10 @@ function build_form_field_conf(gobj, field_desc) {
5784
5798
  }
5785
5799
  break;
5786
5800
  case "hook": break;
5787
- case "fkey": break;
5801
+ case "fkey":
5802
+ field_conf.tag = "select2";
5803
+ field_conf.options = get_fkey_select_options(gobj, field_desc);
5804
+ break;
5788
5805
  case "image":
5789
5806
  case "string":
5790
5807
  field_conf.tag = "input";
@@ -5869,6 +5886,112 @@ function build_form_field_conf(gobj, field_desc) {
5869
5886
  }
5870
5887
  return field_conf;
5871
5888
  }
5889
+ /************************************************************
5890
+ * Options for a fkey field: the ids of the parent topic's
5891
+ * rows, taken from the `fkey_options` attr
5892
+ * ({topic_name: [ids or {id} records]}, supplied by the host —
5893
+ * the form never queries the backend or its parent itself).
5894
+ * Read at build time.
5895
+ ************************************************************/
5896
+ function get_fkey_select_options(gobj, field_desc) {
5897
+ let fkey_desc = field_desc.fkey;
5898
+ if (!is_object(fkey_desc) || Object.keys(fkey_desc).length === 0) {
5899
+ log_error(sprintf("%s: fkey field '%s' without fkey mapping", gobj_short_name(gobj), field_desc.name));
5900
+ return [];
5901
+ }
5902
+ let topic_name = Object.keys(fkey_desc)[0];
5903
+ let fkey_options = gobj_read_attr(gobj, "fkey_options");
5904
+ let list = is_object(fkey_options) ? fkey_options[topic_name] : null;
5905
+ if (!is_array(list)) {
5906
+ log_warning(sprintf("%s: no fkey_options for topic '%s' (field '%s')", gobj_short_name(gobj), topic_name, field_desc.name));
5907
+ return [];
5908
+ }
5909
+ let options = [];
5910
+ for (let item of list) {
5911
+ let id = is_object(item) ? item.id : item;
5912
+ if (is_string(id) && !empty_string(id)) options.push(id);
5913
+ }
5914
+ return options;
5915
+ }
5916
+ /************************************************************
5917
+ * Encode the select2 value (array of ids) into fkey ref(s)
5918
+ * "topic_name^id^hook_name", shaped by the column real type:
5919
+ * string -> single ref (or null if no selection)
5920
+ * object/dict -> {ref: true, ...}
5921
+ * array/list -> [ref, ...]
5922
+ ************************************************************/
5923
+ function build_fkey_ref$1(gobj, field_desc, value) {
5924
+ let fkey_desc = field_desc.fkey;
5925
+ if (!is_object(fkey_desc) || Object.keys(fkey_desc).length === 0) {
5926
+ log_error(sprintf("%s: fkey field '%s' without fkey mapping", gobj_short_name(gobj), field_desc.name));
5927
+ return null;
5928
+ }
5929
+ let topic_name = Object.keys(fkey_desc)[0];
5930
+ let hook_name = fkey_desc[topic_name];
5931
+ let refs;
5932
+ switch (field_desc.real_type) {
5933
+ case "string":
5934
+ refs = null;
5935
+ break;
5936
+ case "object":
5937
+ case "dict":
5938
+ refs = {};
5939
+ break;
5940
+ case "array":
5941
+ case "list":
5942
+ refs = [];
5943
+ break;
5944
+ default:
5945
+ log_error(sprintf("%s: fkey field '%s' with bad real_type '%s'", gobj_short_name(gobj), field_desc.name, field_desc.real_type));
5946
+ return null;
5947
+ }
5948
+ if (!is_array(value)) value = value ? [value] : [];
5949
+ for (let v of value) {
5950
+ let id = is_object(v) ? v.id : v;
5951
+ if (!is_string(id) || empty_string(id)) {
5952
+ log_error(sprintf("%s: fkey field '%s' with bad value: %s", gobj_short_name(gobj), field_desc.name, JSON.stringify(v)));
5953
+ continue;
5954
+ }
5955
+ let ref = topic_name + "^" + id + "^" + hook_name;
5956
+ switch (field_desc.real_type) {
5957
+ case "string": return ref;
5958
+ case "object":
5959
+ case "dict":
5960
+ refs[ref] = true;
5961
+ break;
5962
+ case "array":
5963
+ case "list":
5964
+ refs.push(ref);
5965
+ break;
5966
+ }
5967
+ }
5968
+ return refs;
5969
+ }
5970
+ /************************************************************
5971
+ * Apply the form mode to the pkey field:
5972
+ * "update" -> pkey readonly, not required
5973
+ * "create" -> pkey writable and required (readonly if rowid)
5974
+ * Empty form_mode keeps the template-declared behaviour
5975
+ * (backward compatible).
5976
+ ************************************************************/
5977
+ function apply_form_mode(gobj, $form) {
5978
+ let mode = gobj_read_str_attr(gobj, "form_mode");
5979
+ if (empty_string(mode)) return;
5980
+ let pkey = gobj_read_str_attr(gobj, "pkey");
5981
+ let $input = $form.querySelector(`[name="${pkey}"]`);
5982
+ if (!$input) {
5983
+ log_error(sprintf("%s: form_mode '%s' but form has no pkey field '%s'", gobj_short_name(gobj), mode, pkey));
5984
+ return;
5985
+ }
5986
+ let is_rowid = $input.field_desc && $input.field_desc.type === "rowid";
5987
+ if (mode === "create" && !is_rowid) {
5988
+ $input.removeAttribute("readonly");
5989
+ $input.setAttribute("required", "");
5990
+ } else {
5991
+ $input.setAttribute("readonly", "readonly");
5992
+ $input.removeAttribute("required");
5993
+ }
5994
+ }
5872
5995
  /******************************************************************
5873
5996
  * With html field conf builds an html element,
5874
5997
  * ready to add to the form
@@ -5899,7 +6022,7 @@ function build_form_field_conf(gobj, field_desc) {
5899
6022
  * default_value:
5900
6023
  * }
5901
6024
  ******************************************************************/
5902
- function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, name, label, placeholder, options, extras, readonly, required, hidden, default_value }) {
6025
+ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, name, label, label_i18n, placeholder, options, extras, readonly, required, hidden, default_value }) {
5903
6026
  let $element;
5904
6027
  let kw_add_table_row = {
5905
6028
  $row: null,
@@ -5921,7 +6044,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
5921
6044
  "label",
5922
6045
  {
5923
6046
  class: "label ",
5924
- i18n: label,
6047
+ i18n: label_i18n || label,
5925
6048
  for: name
5926
6049
  },
5927
6050
  label
@@ -5955,7 +6078,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
5955
6078
  "label",
5956
6079
  {
5957
6080
  class: "label ",
5958
- i18n: label,
6081
+ i18n: label_i18n || label,
5959
6082
  for: name
5960
6083
  },
5961
6084
  label
@@ -5997,7 +6120,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
5997
6120
  "label",
5998
6121
  {
5999
6122
  class: "label ",
6000
- i18n: label,
6123
+ i18n: label_i18n || label,
6001
6124
  for: name
6002
6125
  },
6003
6126
  label
@@ -6028,7 +6151,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6028
6151
  "label",
6029
6152
  {
6030
6153
  class: "label ",
6031
- i18n: label,
6154
+ i18n: label_i18n || label,
6032
6155
  for: name
6033
6156
  },
6034
6157
  label
@@ -6059,7 +6182,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6059
6182
  "label",
6060
6183
  {
6061
6184
  class: "label",
6062
- i18n: label
6185
+ i18n: label_i18n || label
6063
6186
  },
6064
6187
  label
6065
6188
  ]]
@@ -6143,7 +6266,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6143
6266
  "label",
6144
6267
  {
6145
6268
  class: "label ",
6146
- i18n: label,
6269
+ i18n: label_i18n || label,
6147
6270
  for: name
6148
6271
  },
6149
6272
  label
@@ -6169,7 +6292,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6169
6292
  switch (tag) {
6170
6293
  case "input": {
6171
6294
  let attrs = {
6172
- class: `input ${name === "id" ? "with-focus" : ""}`,
6295
+ class: `input ${name === gobj_read_str_attr(gobj, "pkey") ? "with-focus" : ""}`,
6173
6296
  name,
6174
6297
  type: inputType,
6175
6298
  placeholder
@@ -6222,8 +6345,8 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6222
6345
  $control.appendChild($extend);
6223
6346
  break;
6224
6347
  }
6225
- case "select":
6226
- $extend = createElement2([
6348
+ case "select": {
6349
+ let $wrapper = createElement2([
6227
6350
  "div",
6228
6351
  { class: "select" },
6229
6352
  [
@@ -6236,11 +6359,16 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6236
6359
  i18n: option
6237
6360
  },
6238
6361
  option
6239
- ])
6362
+ ]),
6363
+ { "change": function(evt) {
6364
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
6365
+ } }
6240
6366
  ]
6241
6367
  ]);
6242
- $control.appendChild($extend);
6368
+ $control.appendChild($wrapper);
6369
+ $extend = $wrapper.querySelector("select");
6243
6370
  break;
6371
+ }
6244
6372
  case "select2":
6245
6373
  $extend = createElement2([
6246
6374
  "select",
@@ -6262,6 +6390,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6262
6390
  $extend.tom_select = new TomSelect($extend, {
6263
6391
  allowEmptyOption: true,
6264
6392
  placeholder,
6393
+ maxItems: field_desc.type === "fkey" && field_desc.real_type === "string" ? 1 : null,
6265
6394
  plugins: {
6266
6395
  "clear_button": { "title": t("Remove all selected options") },
6267
6396
  "no_active_items": {},
@@ -6273,17 +6402,26 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6273
6402
  gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
6274
6403
  });
6275
6404
  break;
6276
- case "checkbox":
6277
- $extend = createElement2([
6405
+ case "checkbox": {
6406
+ let $wrapper = createElement2([
6278
6407
  "label",
6279
6408
  { class: "checkbox" },
6280
- ["input", {
6281
- type: "checkbox",
6282
- name
6283
- }]
6409
+ [
6410
+ "input",
6411
+ {
6412
+ type: "checkbox",
6413
+ name
6414
+ },
6415
+ "",
6416
+ { "change": function(evt) {
6417
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
6418
+ } }
6419
+ ]
6284
6420
  ]);
6285
- $control.appendChild($extend);
6421
+ $control.appendChild($wrapper);
6422
+ $extend = $wrapper.querySelector("input");
6286
6423
  break;
6424
+ }
6287
6425
  case "radio":
6288
6426
  $extend = createOneHtml(`
6289
6427
  <label class="radio">
@@ -6296,6 +6434,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6296
6434
  case "jsoneditor": {
6297
6435
  let attrs = {
6298
6436
  class: "jsoneditor jse-theme-dark",
6437
+ name,
6299
6438
  style: ""
6300
6439
  };
6301
6440
  Object.assign(attrs, extras);
@@ -6303,6 +6442,18 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
6303
6442
  else if (required) attrs.required = "";
6304
6443
  $extend = createElement2(["div", attrs]);
6305
6444
  $control.appendChild($extend);
6445
+ $extend.jsoneditor = new JSONEditor({
6446
+ target: $extend,
6447
+ props: {
6448
+ readOnly: !!readonly,
6449
+ onChange: function() {
6450
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
6451
+ },
6452
+ timestampTag: function({ field, value, path }) {
6453
+ 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";
6454
+ }
6455
+ }
6456
+ });
6306
6457
  break;
6307
6458
  }
6308
6459
  case "fieldset": {
@@ -6698,7 +6849,7 @@ function template2columns(gobj, columns, template, sub_elements) {
6698
6849
  /************************************************************
6699
6850
  *
6700
6851
  ************************************************************/
6701
- function validate_form$1(gobj, $form) {
6852
+ function validate_form(gobj, $form) {
6702
6853
  let validated = true;
6703
6854
  if (!$form.checkValidity()) validated = false;
6704
6855
  $form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
@@ -6719,7 +6870,7 @@ function getInputValue($input) {
6719
6870
  /************************************************************
6720
6871
  * Function to get input value based on its type
6721
6872
  ************************************************************/
6722
- function get_form_values$1(gobj, $form) {
6873
+ function get_form_values(gobj, $form) {
6723
6874
  const record = {};
6724
6875
  $form.querySelectorAll(".yui-form-data-input").forEach(($input) => {
6725
6876
  $input.closest(".xfield");
@@ -6800,9 +6951,10 @@ function get_form_values$1(gobj, $form) {
6800
6951
  /************************************************************
6801
6952
  *
6802
6953
  ************************************************************/
6803
- function clear_data$1(gobj, $form) {
6954
+ function clear_data(gobj, $form) {
6955
+ let pkey = gobj_read_str_attr(gobj, "pkey");
6804
6956
  $form.querySelectorAll("input, select, textarea, .jsoneditor").forEach(($input) => {
6805
- if ($input.name !== "id") if ($input.classList.contains("jsoneditor")) {
6957
+ if ($input.name !== pkey) if ($input.classList.contains("jsoneditor")) {
6806
6958
  let jsoneditor = $input.jsoneditor;
6807
6959
  if (jsoneditor) jsoneditor.set({ text: "" });
6808
6960
  } else if ($input.type === "checkbox") $input.checked = false;
@@ -6821,7 +6973,7 @@ function clear_data$1(gobj, $form) {
6821
6973
  /************************************************************
6822
6974
  *
6823
6975
  ************************************************************/
6824
- function clear_validation$1(gobj, $form) {
6976
+ function clear_validation(gobj, $form) {
6825
6977
  $form.querySelectorAll("input").forEach(($input) => {
6826
6978
  $input.classList.remove("is-danger");
6827
6979
  });
@@ -6859,10 +7011,10 @@ function convertColor(color) {
6859
7011
  /************************************************************
6860
7012
  *
6861
7013
  ************************************************************/
6862
- function set_form_values$1(gobj, template, $form, record) {
7014
+ function set_form_values(gobj, template, $form, record) {
6863
7015
  let modified = false;
6864
- clear_data$1(gobj, $form);
6865
- if (!record || record.length === 0) record = create_template_record(template);
7016
+ clear_data(gobj, $form);
7017
+ if (!record || Object.keys(record).length === 0) record = create_template_record(template);
6866
7018
  $form.querySelectorAll(".yui-form-data-input").forEach(($input) => {
6867
7019
  let field_desc = $input.field_desc;
6868
7020
  let name = $input.dataset.treedb_name;
@@ -6878,10 +7030,16 @@ function set_form_values$1(gobj, template, $form, record) {
6878
7030
  modified = true;
6879
7031
  }
6880
7032
  value = treedb_value_2_form_value(gobj, field_desc, value);
7033
+ if (value === void 0) value = null;
6881
7034
  switch (tag) {
6882
7035
  case "input":
6883
7036
  switch (input_type) {
6884
- case "datetime-local": break;
7037
+ case "datetime-local":
7038
+ if (value instanceof Date && !isNaN(value.getTime())) {
7039
+ let pad = (n) => String(n).padStart(2, "0");
7040
+ $input.value = `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
7041
+ } else $input.value = "";
7042
+ break;
6885
7043
  case "color":
6886
7044
  $input.value = convertColor(value);
6887
7045
  break;
@@ -6908,11 +7066,11 @@ function set_form_values$1(gobj, template, $form, record) {
6908
7066
  break;
6909
7067
  case "jsoneditor":
6910
7068
  let jsoneditor = $input.jsoneditor;
6911
- if (jsoneditor) jsoneditor.set(value);
7069
+ if (jsoneditor) jsoneditor.set(value ? value : { text: "" });
6912
7070
  break;
6913
7071
  case "fieldset": break;
6914
7072
  case "table":
6915
- load_tabulator_data(gobj, $input, value);
7073
+ load_tabulator_data(gobj, $input, value ? value : []);
6916
7074
  break;
6917
7075
  case "coordinates":
6918
7076
  if ($input.tagName !== "INPUT") $input = $input.querySelector("input");
@@ -6958,7 +7116,22 @@ function treedb_value_2_form_value(gobj, field_desc, value) {
6958
7116
  let type = field_desc.type;
6959
7117
  field_desc.real_type;
6960
7118
  switch (type) {
6961
- case "fkey":
7119
+ case "fkey": {
7120
+ let new_value = [];
7121
+ if (is_string(value) && !empty_string(value)) {
7122
+ let fkey = treedb_decoder_fkey(field_desc, value);
7123
+ if (fkey) new_value.push(fkey.id);
7124
+ } else if (is_array(value)) for (let v of value) {
7125
+ let fkey = treedb_decoder_fkey(field_desc, v);
7126
+ if (fkey) new_value.push(fkey.id);
7127
+ }
7128
+ else if (is_object(value)) for (let v of Object.keys(value)) {
7129
+ let fkey = treedb_decoder_fkey(field_desc, v);
7130
+ if (fkey) new_value.push(fkey.id);
7131
+ }
7132
+ value = new_value;
7133
+ break;
7134
+ }
6962
7135
  case "hook":
6963
7136
  case "enum":
6964
7137
  case "uuid":
@@ -6968,8 +7141,7 @@ function treedb_value_2_form_value(gobj, field_desc, value) {
6968
7141
  case "url": break;
6969
7142
  case "now":
6970
7143
  case "time":
6971
- value = parseInt(value) || 0;
6972
- value = new Date(value);
7144
+ value = /* @__PURE__ */ new Date((parseInt(value) || 0) * 1e3);
6973
7145
  break;
6974
7146
  case "date":
6975
7147
  case "color":
@@ -6985,13 +7157,19 @@ function treedb_value_2_form_value(gobj, field_desc, value) {
6985
7157
  case "base64": break;
6986
7158
  case "string":
6987
7159
  case "integer":
7160
+ case "real":
7161
+ case "boolean": break;
6988
7162
  case "object":
6989
7163
  case "dict":
7164
+ value = { json: is_object(value) ? value : {} };
7165
+ break;
7166
+ case "blob":
7167
+ value = { json: is_object(value) || is_array(value) ? value : {} };
7168
+ break;
6990
7169
  case "array":
6991
7170
  case "list":
6992
- case "real":
6993
- case "boolean": break;
6994
- case "blob": break;
7171
+ value = { json: is_array(value) ? value : [] };
7172
+ break;
6995
7173
  case "number": break;
6996
7174
  case "coordinates":
6997
7175
  let longitude = 0;
@@ -7023,7 +7201,9 @@ function form_value_2_treedb_value(gobj, field_desc, value) {
7023
7201
  let type = field_desc.type;
7024
7202
  let real_type = field_desc.real_type;
7025
7203
  switch (type) {
7026
- case "fkey": break;
7204
+ case "fkey":
7205
+ value = build_fkey_ref$1(gobj, field_desc, value);
7206
+ break;
7027
7207
  case "hook":
7028
7208
  value = null;
7029
7209
  break;
@@ -7131,7 +7311,9 @@ function ac_load_record(gobj, event, kw, src) {
7131
7311
  gobj_write_attr(gobj, "record", kw);
7132
7312
  let $form = gobj_read_attr(gobj, "$container").querySelector("form");
7133
7313
  gobj_write_bool_attr(gobj, "initialized", false);
7134
- set_changed_stated(gobj, set_form_values$1(gobj, gobj_read_attr(gobj, "template"), $form, kw));
7314
+ let modified = set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, kw);
7315
+ apply_form_mode(gobj, $form);
7316
+ set_changed_stated(gobj, modified);
7135
7317
  gobj_write_bool_attr(gobj, "initialized", true);
7136
7318
  return 0;
7137
7319
  }
@@ -7141,8 +7323,8 @@ function ac_load_record(gobj, event, kw, src) {
7141
7323
  ************************************************************/
7142
7324
  function ac_save_record(gobj, event, kw, src) {
7143
7325
  let $form = gobj_read_attr(gobj, "$container").querySelector("form");
7144
- if (validate_form$1(gobj, $form)) {
7145
- gobj_publish_event(gobj, "EV_SAVE_RECORD", get_form_values$1(gobj, $form));
7326
+ if (validate_form(gobj, $form)) {
7327
+ gobj_publish_event(gobj, "EV_SAVE_RECORD", get_form_values(gobj, $form));
7146
7328
  set_changed_stated(gobj, false);
7147
7329
  } else {
7148
7330
  kw.abort_close = true;
@@ -7156,7 +7338,7 @@ function ac_save_record(gobj, event, kw, src) {
7156
7338
  function ac_undo_record(gobj, event, kw, src) {
7157
7339
  let $form = gobj_read_attr(gobj, "$container").querySelector("form");
7158
7340
  gobj_write_bool_attr(gobj, "initialized", false);
7159
- set_form_values$1(gobj, gobj_read_attr(gobj, "template"), $form, gobj_read_attr(gobj, "record"));
7341
+ set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, gobj_read_attr(gobj, "record"));
7160
7342
  set_changed_stated(gobj, false);
7161
7343
  gobj_write_bool_attr(gobj, "initialized", true);
7162
7344
  return 0;
@@ -7166,8 +7348,8 @@ function ac_undo_record(gobj, event, kw, src) {
7166
7348
  ************************************************************/
7167
7349
  function ac_clear_record(gobj, event, kw, src) {
7168
7350
  let $form = gobj_read_attr(gobj, "$container").querySelector("form");
7169
- clear_validation$1(gobj, $form);
7170
- clear_data$1(gobj, $form);
7351
+ clear_validation(gobj, $form);
7352
+ clear_data(gobj, $form);
7171
7353
  return 0;
7172
7354
  }
7173
7355
  /************************************************************
@@ -7212,8 +7394,8 @@ function ac_record_changed(gobj, event, kw, src) {
7212
7394
  ************************************************************/
7213
7395
  function ac_copy_record(gobj, event, kw, src) {
7214
7396
  let $form = gobj_read_attr(gobj, "$container").querySelector("form");
7215
- if (validate_form$1(gobj, $form)) {
7216
- let data = { record: get_form_values$1(gobj, $form) };
7397
+ if (validate_form(gobj, $form)) {
7398
+ let data = { record: get_form_values(gobj, $form) };
7217
7399
  navigator.clipboard.writeText(JSON.stringify(data));
7218
7400
  }
7219
7401
  return 0;
@@ -7226,7 +7408,7 @@ function ac_paste_record(gobj, event, kw, src) {
7226
7408
  let record = kw.record;
7227
7409
  if (is_object(record)) {
7228
7410
  gobj_write_bool_attr(gobj, "initialized", true);
7229
- set_form_values$1(gobj, gobj_read_attr(gobj, "template"), $form, record);
7411
+ set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, record);
7230
7412
  }
7231
7413
  return 0;
7232
7414
  }
@@ -7372,6 +7554,7 @@ function create_gclass$11(gclass_name) {
7372
7554
  * Register GClass
7373
7555
  ***************************************************************/
7374
7556
  function register_c_yui_form() {
7557
+ if (gclass_find_by_name(GCLASS_NAME$11, false)) return 0;
7375
7558
  return create_gclass$11(GCLASS_NAME$11);
7376
7559
  }
7377
7560
  //#endregion
@@ -8449,7 +8632,7 @@ function onClick(gobj, e) {
8449
8632
  const map = gobj.priv.xmap;
8450
8633
  const coordinates = e.features[0].geometry.coordinates.slice();
8451
8634
  const properties = e.features[0].properties;
8452
- const gobj_service = gobj_find_service(properties.gobj_service_name, true);
8635
+ const gobj_service = properties.gobj_service_name ? gobj_find_service(properties.gobj_service_name, false) : null;
8453
8636
  if (gobj_service) {
8454
8637
  let window_service_name = `window-map-${clean_name(gobj_name(gobj_service))}`;
8455
8638
  let popupContent = createElement2([
@@ -11743,6 +11926,93 @@ function resolve_route(item_index, route) {
11743
11926
  };
11744
11927
  }
11745
11928
  //#endregion
11929
+ //#region src/shell_section_index.js
11930
+ /***********************************************************************
11931
+ * shell_section_index.js
11932
+ *
11933
+ * Pure helper for C_YUI_SHELL: synthesize the stage target of a
11934
+ * section-index landing route.
11935
+ *
11936
+ * A level-1 menu item that declares `submenu.index` opts out of
11937
+ * the redirect-to-default-child behaviour: its own route becomes
11938
+ * a real resting, deep-linkable route whose view is the submenu
11939
+ * itself rendered as a "cards" C_YUI_NAV (the shell's view
11940
+ * contract only requires a $container by the end of mt_create,
11941
+ * which C_YUI_NAV already satisfies). Config:
11942
+ *
11943
+ * "submenu": {
11944
+ * "render": { "top-sub": "tabs" },
11945
+ * "index": true — landing in stage "main"
11946
+ * "index": {"stage": "x"} — landing in stage "x"
11947
+ * }
11948
+ *
11949
+ * Precedence: an explicit inline `target` on the item wins (this
11950
+ * helper returns null then). When the target is synthesized,
11951
+ * `submenu.default` becomes inert for the section: navigate_to()
11952
+ * only redirects to the default child while the entry has NO
11953
+ * target.
11954
+ *
11955
+ * Tabs and cards never coexist (DRY of navigation): while the
11956
+ * index is on stage the shell collapses the secondary zone, and
11957
+ * for index sections the tab strip defaults to desktop/tablet
11958
+ * only — on mobile a "backbar" nav (← <section>) takes its place
11959
+ * inside a child view. See secondary_nav_renders().
11960
+ *
11961
+ * Split out of c_yui_shell.js so it can be unit-tested without
11962
+ * a DOM.
11963
+ *
11964
+ * Copyright (c) 2026, ArtGins.
11965
+ * All Rights Reserved.
11966
+ ***********************************************************************/
11967
+ /************************************************************
11968
+ * Return the synthesized target for `item`'s section route,
11969
+ * or null when the item doesn't opt in.
11970
+ ************************************************************/
11971
+ function section_index_target(menu_id, item) {
11972
+ if (!item || !item.route || item.target) return null;
11973
+ let sub = item.submenu;
11974
+ if (!sub || !Array.isArray(sub.items) || !sub.index) return null;
11975
+ return {
11976
+ stage: (typeof sub.index === "object" ? sub.index : {}).stage || "main",
11977
+ gclass: "C_YUI_NAV",
11978
+ kw: {
11979
+ menu_id: `index.${menu_id}.${item.id}`,
11980
+ nav_label: item.name || item.id || "",
11981
+ menu_items: sub.items,
11982
+ layout: "cards",
11983
+ level: "secondary"
11984
+ }
11985
+ };
11986
+ }
11987
+ /************************************************************
11988
+ * Return the list of render configs for one (submenu, zone)
11989
+ * pair. `layout` is the raw submenu.render[zone] value
11990
+ * (string or object).
11991
+ *
11992
+ * Without submenu.index: the declared render, unchanged.
11993
+ * With submenu.index (list → detail):
11994
+ * - the declared nav defaults to show_on ">=tablet" (an
11995
+ * explicit show_on in the render object wins);
11996
+ * - a "backbar" nav (show_on "<tablet") is appended so a
11997
+ * child view offers "← <section>" on mobile instead of
11998
+ * the duplicated tab strip. Disable with
11999
+ * index: {backbar: false}; tune its breakpoints with
12000
+ * index: {backbar: {show_on: "..."}}.
12001
+ ************************************************************/
12002
+ function secondary_nav_renders(item, layout) {
12003
+ let cfg = layout && typeof layout === "object" ? Object.assign({}, layout) : { layout: String(layout || "vertical") };
12004
+ let sub = item && item.submenu || {};
12005
+ if (!sub.index) return [cfg];
12006
+ let index_cfg = typeof sub.index === "object" ? sub.index : {};
12007
+ if (!cfg.show_on) cfg.show_on = ">=tablet";
12008
+ if (index_cfg.backbar === false) return [cfg];
12009
+ return [cfg, {
12010
+ layout: "backbar",
12011
+ show_on: (typeof index_cfg.backbar === "object" && index_cfg.backbar ? index_cfg.backbar : {}).show_on || "<tablet",
12012
+ back_route: item && item.route || ""
12013
+ }];
12014
+ }
12015
+ //#endregion
11746
12016
  //#region src/c_yui_shell.js
11747
12017
  /***********************************************************************
11748
12018
  * c_yui_shell.js
@@ -11762,6 +12032,11 @@ function resolve_route(item_index, route) {
11762
12032
  * $container in its stage, hide the previous one. Lifecycle per item
11763
12033
  * decides when the gobj is created/destroyed.
11764
12034
  *
12035
+ * A primary item with `submenu.index` gets a section-index landing:
12036
+ * its own route mounts the submenu as a "cards" C_YUI_NAV in the
12037
+ * stage instead of redirecting to the default child (see
12038
+ * shell_section_index.js for the contract).
12039
+ *
11765
12040
  * Hash-based 2-level routing (#/primary/secondary). No dependency on
11766
12041
  * C_YUI_ROUTING.
11767
12042
  *
@@ -12137,12 +12412,13 @@ function build_item_index(gobj, config) {
12137
12412
  let items = menu && menu.items || [];
12138
12413
  for (let item of items) {
12139
12414
  if (item.route) {
12415
+ let target = item.target || section_index_target(menu_id, item);
12140
12416
  let prev = priv.item_index[item.route];
12141
- if (!prev || !prev.target && item.target) priv.item_index[item.route] = {
12417
+ if (!prev || !prev.target && target) priv.item_index[item.route] = {
12142
12418
  item,
12143
12419
  parent_item: null,
12144
- stage: item.target && item.target.stage || null,
12145
- target: item.target || null,
12420
+ stage: target && target.stage || null,
12421
+ target,
12146
12422
  menu_id
12147
12423
  };
12148
12424
  }
@@ -12220,11 +12496,15 @@ function instantiate_menus(gobj, config) {
12220
12496
  log_warning(`C_YUI_SHELL: submenu of '${menu_id}.${item.id}' renders in unknown zone '${zone_id}'`);
12221
12497
  continue;
12222
12498
  }
12223
- let $c = gobj_read_attr(instantiate_nav_in_zone(gobj, {
12224
- items: sub.items,
12225
- render: { [zone_id]: render_to_obj(layout) }
12226
- }, `secondary.${menu_id}.${item.id}`, zone_id, "secondary", item.name || ""), "$container");
12227
- if ($c) $c.classList.add("is-hidden");
12499
+ let renders = secondary_nav_renders(item, render_to_obj(layout));
12500
+ let sub_menu_id = `secondary.${menu_id}.${item.id}`;
12501
+ for (let render_cfg of renders) {
12502
+ let $c = gobj_read_attr(instantiate_nav_in_zone(gobj, {
12503
+ items: sub.items,
12504
+ render: { [zone_id]: render_cfg }
12505
+ }, sub_menu_id, zone_id, "secondary", item.name || ""), "$container");
12506
+ if ($c) $c.classList.add("is-hidden");
12507
+ }
12228
12508
  }
12229
12509
  }
12230
12510
  }
@@ -12238,9 +12518,8 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
12238
12518
  let priv = gobj_read_attr(gobj, "priv");
12239
12519
  let render_cfg = menu.render && (menu.render[zone_id] || menu.render["*"]) || { layout: "vertical" };
12240
12520
  if (is_string(render_cfg)) render_cfg = { layout: render_cfg };
12241
- let nav_name = `nav_${menu_id.replace(/\./g, "_")}_${zone_id}`;
12242
12521
  let layout = render_cfg.layout || "vertical";
12243
- let nav = gobj_create(nav_name, "C_YUI_NAV", {
12522
+ let nav = gobj_create(`nav_${menu_id.replace(/\./g, "_")}_${zone_id}` + (layout === "backbar" ? "_backbar" : ""), "C_YUI_NAV", {
12244
12523
  menu_id,
12245
12524
  nav_label: nav_label || "",
12246
12525
  menu_items: menu.items || [],
@@ -12249,6 +12528,8 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
12249
12528
  icon_pos: render_cfg.icon_pos || default_icon_pos(zone_id),
12250
12529
  show_label: render_cfg.show_label !== false,
12251
12530
  level,
12531
+ show_on: render_cfg.show_on || "",
12532
+ back_route: render_cfg.back_route || "",
12252
12533
  shell: gobj
12253
12534
  }, gobj);
12254
12535
  let $c = gobj_read_attr(nav, "$container");
@@ -12433,6 +12714,8 @@ function build_view_gobj(gobj, entry, route, stage) {
12433
12714
  }
12434
12715
  stage.el.appendChild($view);
12435
12716
  gobj_start(view);
12717
+ let priv = gobj_read_attr(gobj, "priv");
12718
+ if (priv && typeof priv.translator === "function" && target.gclass === "C_YUI_NAV") refresh_language($view, priv.translator);
12436
12719
  return view;
12437
12720
  }
12438
12721
  function safe_id(s) {
@@ -13017,7 +13300,7 @@ function update_secondary_nav_visibility(gobj, entry) {
13017
13300
  let active_primary_id = entry.parent_item && entry.parent_item.id || entry.item && entry.item.id || "";
13018
13301
  let target_secondary_id = `secondary.${entry.menu_id || ""}.${active_primary_id}`;
13019
13302
  let active_primary = entry.parent_item || entry.item || null;
13020
- let has_secondary = !!(active_primary && active_primary.submenu && Array.isArray(active_primary.submenu.items) && active_primary.submenu.items.some((it) => it && it.route));
13303
+ 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));
13021
13304
  let config = gobj_read_attr(gobj, "config") || {};
13022
13305
  let zones_cfg = config.shell && config.shell.zones || {};
13023
13306
  for (let z in zones_cfg) if (zones_cfg[z] && zones_cfg[z].host === "menu.secondary") {
@@ -13030,7 +13313,7 @@ function update_secondary_nav_visibility(gobj, entry) {
13030
13313
  if (!nav_menu_id.startsWith("secondary.")) continue;
13031
13314
  let $c = gobj_read_attr(nav, "$container");
13032
13315
  if (!$c) continue;
13033
- if (nav_menu_id === target_secondary_id) $c.classList.remove("is-hidden");
13316
+ if (nav_menu_id === target_secondary_id && has_secondary) $c.classList.remove("is-hidden");
13034
13317
  else $c.classList.add("is-hidden");
13035
13318
  }
13036
13319
  }
@@ -13215,6 +13498,86 @@ function yui_shell_close_dropdown(shell_gobj) {
13215
13498
  close_toolbar_dropdown(shell_gobj);
13216
13499
  }
13217
13500
  //#endregion
13501
+ //#region src/nav_cards_helpers.js
13502
+ /***********************************************************************
13503
+ * nav_cards_helpers.js
13504
+ *
13505
+ * Pure descriptor builders for C_YUI_NAV's "cards" layout — a
13506
+ * grid of tappable cards, one per item, used as the section-index
13507
+ * landing of a submenu (list → detail pattern). Split out of
13508
+ * c_yui_nav.js so it can be unit-tested without a DOM: the
13509
+ * functions return createElement2 node descriptors, never
13510
+ * HTMLElements.
13511
+ *
13512
+ * Copyright (c) 2026, ArtGins.
13513
+ * All Rights Reserved.
13514
+ ***********************************************************************/
13515
+ /************************************************************
13516
+ * Descriptor of one card: an <a> carrying the same data-*
13517
+ * contract as every other nav item (data-route/data-item-id/
13518
+ * data-disabled), so C_YUI_NAV's delegated click handler and
13519
+ * active-route highlight work unchanged.
13520
+ ************************************************************/
13521
+ function card_descriptor(it, show_label) {
13522
+ let children = [];
13523
+ let label = it.name || "";
13524
+ if (it.icon) children.push([
13525
+ "span",
13526
+ { class: "icon is-medium" },
13527
+ ["i", {
13528
+ class: it.icon,
13529
+ "aria-hidden": "true"
13530
+ }]
13531
+ ]);
13532
+ if (show_label && label) children.push([
13533
+ "span",
13534
+ {
13535
+ class: "yui-nav-label",
13536
+ i18n: label
13537
+ },
13538
+ label
13539
+ ]);
13540
+ let a_attrs = {
13541
+ class: "yui-nav-item yui-nav-card",
13542
+ href: it.route ? "#" + it.route : "#",
13543
+ "data-item-id": it.id,
13544
+ "data-route": it.route || "",
13545
+ "data-disabled": it.disabled ? "1" : "0",
13546
+ "aria-label": label || it.id
13547
+ };
13548
+ if (label) a_attrs["data-i18n-aria-label"] = label;
13549
+ let tip = it.tooltip || it.aria_label;
13550
+ if (tip) {
13551
+ a_attrs.title = tip;
13552
+ a_attrs["data-i18n-title"] = tip;
13553
+ }
13554
+ if (it.disabled) {
13555
+ a_attrs["aria-disabled"] = "true";
13556
+ a_attrs["tabindex"] = "-1";
13557
+ }
13558
+ return [
13559
+ "a",
13560
+ a_attrs,
13561
+ children
13562
+ ];
13563
+ }
13564
+ /************************************************************
13565
+ * Descriptor of the whole grid. Decorative items (`header`/
13566
+ * `divider`) are dropped, same policy as the tabs layout.
13567
+ ************************************************************/
13568
+ function cards_grid_descriptor(items, show_label) {
13569
+ let cards = [];
13570
+ for (let it of items || []) {
13571
+ if (!it || it.type === "header" || it.type === "divider") continue;
13572
+ cards.push(card_descriptor(it, show_label));
13573
+ }
13574
+ return [
13575
+ "div",
13576
+ {},
13577
+ cards
13578
+ ];
13579
+ }
13580
+ //#endregion
13218
13581
  //#region src/c_yui_nav.js
13219
13582
  /***********************************************************************
13220
13583
  * c_yui_nav.js
@@ -13230,6 +13593,17 @@ function yui_shell_close_dropdown(shell_gobj) {
13230
13593
  * "drawer" — off-canvas vertical (toggled by hamburger)
13231
13594
  * "submenu" — Bulma .menu nested under a heading
13232
13595
  * "accordion" — Bulma .menu list with collapsible group
13596
+ * "cards" — grid of tappable cards (section-index landing;
13597
+ * mounted as a stage view by C_YUI_SHELL when a
13598
+ * primary item declares submenu.index)
13599
+ * "backbar" — single "← <section>" link back to the section
13600
+ * index; the mobile replacement of the tab strip
13601
+ * for submenu.index sections (the shell collapses
13602
+ * the secondary zone while the index is on stage)
13603
+ *
13604
+ * The `show_on` attr (same expressions as the shell's zone
13605
+ * `show_on`) applies breakpoint visibility classes to the nav
13606
+ * container; build_ui re-applies them on every rebuild.
13233
13607
  *
13234
13608
  * Each item supports:
13235
13609
  * id, name, icon (CSS class or svg id), route, badge, disabled
@@ -13267,7 +13641,9 @@ var SUPPORTED_LAYOUTS = [
13267
13641
  "tabs",
13268
13642
  "drawer",
13269
13643
  "submenu",
13270
- "accordion"
13644
+ "accordion",
13645
+ "cards",
13646
+ "backbar"
13271
13647
  ];
13272
13648
  function is_decorative(it) {
13273
13649
  return !!(it && (it.type === "header" || it.type === "divider"));
@@ -13285,6 +13661,8 @@ var attrs_table$4 = [
13285
13661
  SDATA(data_type_t.DTP_STRING, "icon_pos", 0, "left", "left|right|top|bottom"),
13286
13662
  SDATA(data_type_t.DTP_BOOLEAN, "show_label", 0, true, "Show item label next to/under the icon"),
13287
13663
  SDATA(data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
13664
+ SDATA(data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
13665
+ SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
13288
13666
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
13289
13667
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
13290
13668
  SDATA(data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
@@ -13368,11 +13746,23 @@ function build_ui$4(gobj) {
13368
13746
  case "accordion":
13369
13747
  $container = render_accordion(gobj, items);
13370
13748
  break;
13749
+ case "cards":
13750
+ $container = render_cards(gobj, items);
13751
+ break;
13752
+ case "backbar":
13753
+ $container = render_backbar(gobj);
13754
+ break;
13371
13755
  default: $container = render_vertical(gobj, items);
13372
13756
  }
13373
13757
  $container.setAttribute("data-nav-zone", zone);
13374
13758
  $container.setAttribute("data-nav-layout", layout);
13375
13759
  $container.classList.add("C_YUI_NAV", "yui-nav", `yui-nav-${layout}`);
13760
+ let show_on = gobj_read_attr(gobj, "show_on") || "";
13761
+ if (!empty_string(show_on) && show_on !== "*") {
13762
+ let visible = breakpoints_from_expr(show_on);
13763
+ for (let bp of BULMA_BP_ORDER) if (!visible[bp]) $container.classList.add(bulma_hidden_class(bp));
13764
+ $container.setAttribute("data-show-on", show_on);
13765
+ }
13376
13766
  if (layout !== "drawer") {
13377
13767
  $container.setAttribute("role", "navigation");
13378
13768
  if (!$container.hasAttribute("aria-label")) {
@@ -13543,6 +13933,51 @@ function render_tabs(gobj, items) {
13543
13933
  ]
13544
13934
  ]);
13545
13935
  }
13936
+ function render_cards(gobj, items) {
13937
+ return createElement2(cards_grid_descriptor(items, gobj_read_attr(gobj, "show_label")));
13938
+ }
13939
+ /************************************************************
13940
+ * "backbar": a single "← <section>" link back to the section
13941
+ * index route. Static DOM — the active child view names itself
13942
+ * in its own content, so the bar only carries the way back plus
13943
+ * section context. While the index itself is on stage the shell
13944
+ * collapses the whole secondary zone (tabs and cards never
13945
+ * coexist), so the bar needs no self-hiding logic.
13946
+ ************************************************************/
13947
+ function render_backbar(gobj) {
13948
+ let back_route = gobj_read_attr(gobj, "back_route") || "";
13949
+ let label = gobj_read_attr(gobj, "nav_label") || "";
13950
+ let label_attrs = { class: "yui-nav-label" };
13951
+ if (label) label_attrs.i18n = label;
13952
+ let a_attrs = {
13953
+ class: "yui-nav-item yui-nav-back",
13954
+ href: back_route ? "#" + back_route : "#",
13955
+ "data-item-id": "__index__",
13956
+ "data-route": back_route,
13957
+ "aria-label": label || "back"
13958
+ };
13959
+ if (label) a_attrs["data-i18n-aria-label"] = label;
13960
+ return createElement2([
13961
+ "div",
13962
+ {},
13963
+ [
13964
+ "a",
13965
+ a_attrs,
13966
+ [[
13967
+ "span",
13968
+ { class: "icon is-small" },
13969
+ ["i", {
13970
+ class: "yi-arrow-left",
13971
+ "aria-hidden": "true"
13972
+ }]
13973
+ ], [
13974
+ "span",
13975
+ label_attrs,
13976
+ label
13977
+ ]]
13978
+ ]
13979
+ ]);
13980
+ }
13546
13981
  function render_drawer(gobj, items) {
13547
13982
  let menu_id = gobj_read_attr(gobj, "menu_id") || "drawer";
13548
13983
  let wrap = document.createElement("div");
@@ -13844,6 +14279,7 @@ function ac_route_changed(gobj, event, kw, src) {
13844
14279
  gobj_write_attr(gobj, "active_route", route);
13845
14280
  let $c = gobj_read_attr(gobj, "$container");
13846
14281
  if (!$c) return 0;
14282
+ if (gobj_read_attr(gobj, "layout") === "backbar") return 0;
13847
14283
  $c.querySelectorAll(".is-active").forEach((n) => n.classList.remove("is-active"));
13848
14284
  let id_to_mark = null;
13849
14285
  if (level === "primary") {
@@ -14813,7 +15249,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
14813
15249
  * record
14814
15250
  * }
14815
15251
  ********************************************/
14816
- function ac_create_record$1(gobj, event, kw, src) {
15252
+ function ac_create_record(gobj, event, kw, src) {
14817
15253
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
14818
15254
  let topic_name = gobj_read_attr(src, "topic_name");
14819
15255
  let record = kw.record;
@@ -14830,7 +15266,7 @@ function ac_create_record$1(gobj, event, kw, src) {
14830
15266
  * record
14831
15267
  * }
14832
15268
  ********************************************/
14833
- function ac_update_record$1(gobj, event, kw, src) {
15269
+ function ac_update_record(gobj, event, kw, src) {
14834
15270
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
14835
15271
  let topic_name = gobj_read_attr(src, "topic_name");
14836
15272
  let record = kw.record;
@@ -14903,12 +15339,12 @@ function create_gclass$3(gclass_name) {
14903
15339
  ],
14904
15340
  [
14905
15341
  "EV_CREATE_RECORD",
14906
- ac_create_record$1,
15342
+ ac_create_record,
14907
15343
  null
14908
15344
  ],
14909
15345
  [
14910
15346
  "EV_UPDATE_RECORD",
14911
- ac_update_record$1,
15347
+ ac_update_record,
14912
15348
  null
14913
15349
  ],
14914
15350
  [
@@ -15007,7 +15443,6 @@ var attrs_table$2 = [
15007
15443
  SDATA(data_type_t.DTP_POINTER, "tabulator", 0, null, "Tabulator instance"),
15008
15444
  SDATA(data_type_t.DTP_STRING, "table_id", 0, null, "Table div ID"),
15009
15445
  SDATA(data_type_t.DTP_STRING, "toolbar_id", 0, null, "Toolbar ID"),
15010
- SDATA(data_type_t.DTP_STRING, "form_id", 0, null, "Edit form ID"),
15011
15446
  SDATA(data_type_t.DTP_STRING, "popup_id", 0, null, "Edit form popup ID"),
15012
15447
  SDATA_END()
15013
15448
  ];
@@ -15015,7 +15450,9 @@ var PRIVATE_DATA$2 = {
15015
15450
  $container: null,
15016
15451
  treedb_name: "",
15017
15452
  topic_name: "",
15018
- tabulator: null
15453
+ tabulator: null,
15454
+ form: null,
15455
+ $popup: null
15019
15456
  };
15020
15457
  var __gclass__$2 = null;
15021
15458
  function popup_mount_layer() {
@@ -15031,7 +15468,6 @@ function mt_create$2(gobj) {
15031
15468
  let name = clean_name(gobj_name(gobj));
15032
15469
  gobj_write_attr(gobj, "table_id", "table" + name);
15033
15470
  gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
15034
- gobj_write_attr(gobj, "form_id", "form" + name);
15035
15471
  gobj_write_attr(gobj, "popup_id", "popup" + name);
15036
15472
  build_ui$2(gobj);
15037
15473
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
@@ -15048,6 +15484,7 @@ function mt_start$2(gobj) {
15048
15484
  * Framework Method: Stop
15049
15485
  ***************************************************************/
15050
15486
  function mt_stop$2(gobj) {
15487
+ close_form_dialog(gobj);
15051
15488
  table__destroy(gobj);
15052
15489
  }
15053
15490
  /***************************************************************
@@ -15387,6 +15824,29 @@ function col_label(col, topic_name) {
15387
15824
  return t(topic_name ? [topic_name + "." + col.id, col.id] : [col.id], { defaultValue: col.header || col.id });
15388
15825
  }
15389
15826
  /******************************************************************
15827
+ * Column-title formatter: render the header as a span carrying
15828
+ * `data-i18n` (the shared col id) so a live language switch —
15829
+ * which calls refresh_language(document.body) — retranslates the
15830
+ * Tabulator headers in place, without rebuilding the table. Only
15831
+ * tag translatable columns; otherwise keep the schema header
15832
+ * (refresh_language would otherwise overwrite it with the raw id).
15833
+ * Tabulator re-runs this on redraw, so the full cascade re-applies.
15834
+ ******************************************************************/
15835
+ function col_title_formatter(gobj, col) {
15836
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
15837
+ return function(cell, formatterParams, onRendered) {
15838
+ let text = col_label(col, topic_name);
15839
+ let attrs = {};
15840
+ const NO_I18N = "\0";
15841
+ if (t(col.id, { defaultValue: NO_I18N }) !== NO_I18N) attrs.i18n = col.id;
15842
+ return createElement2([
15843
+ "span",
15844
+ attrs,
15845
+ text
15846
+ ]);
15847
+ };
15848
+ }
15849
+ /******************************************************************
15390
15850
  * Build table with Tabulator
15391
15851
  * This fn is called on start and when desc attribute is set.
15392
15852
  * desc contains the description (columns) of table to create
@@ -15491,6 +15951,7 @@ function create_tabulator(gobj) {
15491
15951
  }
15492
15952
  let colDef = {
15493
15953
  title: col_label(col, gobj_read_str_attr(gobj, "topic_name")),
15954
+ titleFormatter: col_title_formatter(gobj, col),
15494
15955
  field: col.id,
15495
15956
  sorter,
15496
15957
  hozAlign,
@@ -15543,7 +16004,13 @@ function create_tabulator(gobj) {
15543
16004
  selectableRows: selectable,
15544
16005
  footerElement: "<span class='yui-tabulator-rowcount' style='display:block;text-align:right;font-size:0.8rem;color:#6b7280;padding:0 0.6rem;'></span>"
15545
16006
  });
15546
- let tabulator = new TabulatorFull(`#${table_id}`, tabulator_settings);
16007
+ let $view_container = gobj_read_attr(gobj, "$container");
16008
+ let $table_el = $view_container ? $view_container.querySelector(`#${table_id}`) : null;
16009
+ if (!$table_el) {
16010
+ log_error(`${gobj_short_name(gobj)}: table element '#${table_id}' not found in $container`);
16011
+ return;
16012
+ }
16013
+ let tabulator = new TabulatorFull($table_el, tabulator_settings);
15547
16014
  function update_rowcount() {
15548
16015
  let $rc = tabulator.element && tabulator.element.querySelector(".yui-tabulator-rowcount");
15549
16016
  if (!$rc) return;
@@ -15678,436 +16145,62 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
15678
16145
  }
15679
16146
  return value;
15680
16147
  }
15681
- /******************************************************************
15682
- * Build the topic form with bulma and select2
15683
- ******************************************************************/
15684
- function build_topic_form(gobj) {
16148
+ /************************************************************
16149
+ * Build the form template from desc.cols: only user-editable
16150
+ * fields reach the form — writable cols, fkeys (linkable) and
16151
+ * the pkey (the hosted C_YUI_FORM's form_mode drives the
16152
+ * pkey's readonly/required state).
16153
+ ************************************************************/
16154
+ function build_form_template(gobj) {
15685
16155
  let desc = gobj_read_attr(gobj, "desc");
15686
- let $form = createElement2(["form", {
15687
- id: gobj_read_str_attr(gobj, "form_id"),
15688
- class: "box p-3",
15689
- novalidate: ""
15690
- }]);
15691
- for (let i = 0; i < desc.cols.length; i++) {
15692
- let col = desc.cols[i];
15693
- if (col.id.charAt(0) === "_") continue;
15694
- let [tag, form_input_type, options, extras] = transform__treedb_type_2_form_type(gobj, col);
15695
- if (!tag) continue;
15696
- let id = col.id;
15697
- let flag = col.flag;
15698
- let is_writable = str_in_list(flag, "writable");
15699
- let is_notnull = str_in_list(flag, "notnull");
15700
- let is_rowid = str_in_list(flag, "rowid");
15701
- let is_required = str_in_list(flag, "required");
15702
- let is_hidden = str_in_list(flag, "hidden");
15703
- str_in_list(flag, "persistent");
15704
- str_in_list(flag, "hook");
15705
- let is_fkey = str_in_list(flag, "fkey");
15706
- if (id === desc.pkey) is_writable = false;
15707
- if (is_fkey) is_writable = true;
15708
- if (!is_writable) {
15709
- if (id !== desc.pkey) continue;
15710
- }
15711
- if (is_hidden) continue;
15712
- if (is_rowid) {
15713
- is_writable = false;
15714
- is_required = false;
15715
- is_notnull = false;
15716
- }
15717
- let field_conf = {
15718
- tag,
15719
- inputType: form_input_type,
15720
- name: col.id,
15721
- label: col_label(col, gobj_read_str_attr(gobj, "topic_name")),
15722
- placeholder: "",
15723
- options,
15724
- extras,
15725
- readonly: !is_writable,
15726
- required: is_required,
15727
- hidden: is_hidden,
15728
- not_null: is_notnull,
15729
- is_pkey: id === desc.pkey,
15730
- is_rowid
15731
- };
15732
- let $field = create_html_field(field_conf);
15733
- $field.field_conf = field_conf;
15734
- $form.appendChild($field);
16156
+ let template = [];
16157
+ for (let col of desc.cols) {
16158
+ if (!col.id || col.id.charAt(0) === "_") continue;
16159
+ const field_desc = treedb_get_field_desc(col);
16160
+ if (field_desc.is_hidden) continue;
16161
+ if (field_desc.type === "hook") continue;
16162
+ if (!field_desc.is_writable && field_desc.type !== "fkey" && col.id !== desc.pkey) continue;
16163
+ template.push(col);
15735
16164
  }
15736
- return $form;
16165
+ return template;
15737
16166
  }
15738
16167
  /************************************************************
15739
- * Convert a col type from backend to frontend
15740
- * Return data to build the HTMLElement
15741
- *
15742
- * Options format for select/select2:
15743
- * <option value="reading">Reading</option>
16168
+ * Collect the linkable parent rows for every fkey col:
16169
+ * {topic_name: rows}, asked to the topics manager (parent)
16170
+ * with the same command the old embedded form used. Collected
16171
+ * fresh on every dialog open, so new parent rows are always
16172
+ * offered.
15744
16173
  ************************************************************/
15745
- function transform__treedb_type_2_form_type(gobj, col) {
15746
- const field_desc = treedb_get_field_desc(col);
15747
- let tag = null;
15748
- let inputType = "";
15749
- let options = [];
15750
- let extras = {};
15751
- switch (field_desc.type) {
15752
- case "image":
15753
- case "string":
15754
- tag = "input";
15755
- inputType = "text";
15756
- break;
15757
- case "integer":
15758
- tag = "input";
15759
- extras.inputmode = "numeric";
15760
- break;
15761
- case "real":
15762
- tag = "input";
15763
- extras.inputmode = "decimal";
15764
- break;
15765
- case "rowid":
15766
- tag = "input";
15767
- inputType = "text";
15768
- break;
15769
- case "boolean":
15770
- tag = "checkbox";
15771
- break;
15772
- case "now":
15773
- case "time":
15774
- tag = "input";
15775
- inputType = "datetime-local";
15776
- switch (field_desc.real_type) {
15777
- case "string": break;
15778
- case "integer": break;
15779
- }
15780
- break;
15781
- case "color":
15782
- tag = "input";
15783
- inputType = "color";
15784
- switch (field_desc.real_type) {
15785
- case "string": break;
15786
- case "integer": break;
15787
- }
15788
- break;
15789
- case "email":
15790
- tag = "input";
15791
- inputType = "email";
15792
- extras.inputmode = "email";
15793
- break;
15794
- case "password":
15795
- tag = "input";
15796
- inputType = "password";
15797
- break;
15798
- case "url":
15799
- tag = "input";
15800
- inputType = "url";
15801
- extras.inputmode = "url";
15802
- break;
15803
- case "tel":
15804
- tag = "input";
15805
- inputType = "tel";
15806
- extras.inputmode = "tel";
15807
- break;
15808
- case "object":
15809
- case "dict":
15810
- case "template":
15811
- case "array":
15812
- case "list":
15813
- case "coordinates":
15814
- case "blob":
15815
- tag = "jsoneditor";
15816
- break;
15817
- case "enum":
15818
- tag = "select2";
15819
- switch (field_desc.real_type) {
15820
- case "string":
15821
- case "object":
15822
- case "dict":
15823
- case "array":
15824
- case "list":
15825
- options = list2options(field_desc.enum_list, "id", "id");
15826
- break;
15827
- }
15828
- break;
15829
- case "hook": break;
15830
- case "fkey":
15831
- tag = "select2";
15832
- switch (field_desc.real_type) {
15833
- case "string":
15834
- case "object":
15835
- case "dict":
15836
- case "array":
15837
- case "list":
15838
- options = get_fkey_options(gobj, col);
15839
- break;
15840
- }
15841
- break;
15842
- }
15843
- return [
15844
- tag,
15845
- inputType,
15846
- options,
15847
- extras
15848
- ];
15849
- }
15850
- /******************************************************************
15851
- * Helper function to create different form elements
15852
- * Available types:
15853
- * - input
15854
- * - textarea
15855
- * - select
15856
- * - select2
15857
- * - checkbox
15858
- * - radio
15859
- * - jsoneditor
15860
- *
15861
- * {
15862
- * tag: field tag,
15863
- * inputType: input type,
15864
- * name: id
15865
- * label: ""
15866
- * placeholder: "",
15867
- * options: [{id:"", value:""}]]
15868
- * extras: {}
15869
- * readonly: bool
15870
- * required: bool
15871
- * hidden: bool
15872
- * not_null: bool
15873
- * _col: col
15874
- * }
15875
- ******************************************************************/
15876
- function create_html_field({ tag, inputType, name, label, placeholder, options, extras, readonly, required, hidden, not_null }) {
15877
- let base;
15878
- switch (tag) {
15879
- case "input":
15880
- case "textarea":
15881
- case "select":
15882
- case "select2":
15883
- case "checkbox":
15884
- case "radio":
15885
- case "jsoneditor":
15886
- base = [
15887
- "div",
15888
- { class: "xfield field is-horizontal" },
15889
- [[
15890
- "div",
15891
- { class: "field-label is-normal" },
15892
- [[
15893
- "label",
15894
- {
15895
- class: "label ",
15896
- i18n: label,
15897
- for: name
15898
- },
15899
- label
15900
- ]]
15901
- ], [
15902
- "div",
15903
- { class: "field-body" },
15904
- [[
15905
- "div",
15906
- { class: "field" },
15907
- [[
15908
- "div",
15909
- { class: "control" },
15910
- []
15911
- ]]
15912
- ]]
15913
- ]]
15914
- ];
15915
- break;
15916
- default: base = [
15917
- "div",
15918
- { class: "control" },
15919
- "Unsupported element type"
15920
- ];
15921
- }
15922
- let $element = createElement2(base);
15923
- let $control = $element.querySelector(".control");
15924
- switch (tag) {
15925
- case "input": {
15926
- let attrs = {
15927
- class: `input ${name === "id" ? "with-focus" : ""}`,
15928
- name,
15929
- type: inputType,
15930
- placeholder
15931
- };
15932
- Object.assign(attrs, extras);
15933
- if (readonly) attrs.readonly = "";
15934
- let $extend = createElement2([
15935
- "input",
15936
- attrs,
15937
- "",
15938
- { "blur": function(evt) {
15939
- validate_field_on_blur(this);
15940
- } }
15941
- ]);
15942
- $control.appendChild($extend);
15943
- $control.appendChild(createElement2([
15944
- "p",
15945
- {
15946
- class: "help is-danger",
15947
- style: "display:none"
15948
- },
15949
- ""
15950
- ]));
15951
- break;
15952
- }
15953
- case "jsoneditor": {
15954
- let attrs = {
15955
- class: "jsoneditor jse-theme-dark",
15956
- style: ""
15957
- };
15958
- Object.assign(attrs, extras);
15959
- if (readonly) attrs.readonly = "";
15960
- let $extend = createElement2(["div", attrs]);
15961
- $extend.setAttribute("name", name);
15962
- $control.appendChild($extend);
15963
- $control.appendChild(createElement2([
15964
- "p",
15965
- {
15966
- class: "help is-danger",
15967
- style: "display:none"
15968
- },
15969
- ""
15970
- ]));
15971
- break;
15972
- }
15973
- case "textarea": {
15974
- let attrs = {
15975
- class: "textarea",
15976
- name,
15977
- placeholder
15978
- };
15979
- Object.assign(attrs, extras);
15980
- if (readonly) attrs.readonly = "";
15981
- let $extend = createElement2([
15982
- "textarea",
15983
- attrs,
15984
- "",
15985
- { "blur": function(evt) {
15986
- validate_field_on_blur(this);
15987
- } }
15988
- ]);
15989
- $control.appendChild($extend);
15990
- $control.appendChild(createElement2([
15991
- "p",
15992
- {
15993
- class: "help is-danger",
15994
- style: "display:none"
15995
- },
15996
- ""
15997
- ]));
15998
- break;
15999
- }
16000
- case "select": {
16001
- let $extend = createElement2([
16002
- "div",
16003
- { class: "select" },
16004
- [
16005
- "select",
16006
- { name },
16007
- options.map((option) => [
16008
- "option",
16009
- {
16010
- value: option.id,
16011
- i18n: option.value
16012
- },
16013
- option.value
16014
- ])
16015
- ]
16016
- ]);
16017
- $control.appendChild($extend);
16018
- $control.appendChild(createElement2([
16019
- "p",
16020
- {
16021
- class: "help is-danger",
16022
- style: "display:none"
16023
- },
16024
- ""
16025
- ]));
16026
- break;
16027
- }
16028
- case "select2": {
16029
- let $extend = createElement2([
16030
- "select",
16031
- {
16032
- name,
16033
- class: "select2-multiple",
16034
- multiple: "multiple",
16035
- style: "width: 100% !important;"
16036
- },
16037
- options.map((option) => [
16038
- "option",
16039
- {
16040
- value: option.id,
16041
- i18n: option.value
16042
- },
16043
- option.value
16044
- ])
16045
- ]);
16046
- $control.appendChild($extend);
16047
- $control.appendChild(createElement2([
16048
- "p",
16049
- {
16050
- class: "help is-danger",
16051
- style: "display:none"
16052
- },
16053
- ""
16054
- ]));
16055
- break;
16056
- }
16057
- case "checkbox": {
16058
- let $extend = createElement2([
16059
- "label",
16060
- { class: "checkbox" },
16061
- ["input", {
16062
- type: "checkbox",
16063
- name
16064
- }]
16065
- ]);
16066
- $control.appendChild($extend);
16067
- $control.appendChild(createElement2([
16068
- "p",
16069
- {
16070
- class: "help is-danger",
16071
- style: "display:none"
16072
- },
16073
- ""
16074
- ]));
16075
- break;
16076
- }
16077
- case "radio":
16078
- for (const option of options) {
16079
- let $extend = createOneHtml(`
16080
- <label class="radio">
16081
- <input type="radio" name="${name}">
16082
- ${t(option)}
16083
- </label>
16084
- `);
16085
- $control.appendChild($extend);
16086
- }
16087
- $control.appendChild(createElement2([
16088
- "p",
16089
- {
16090
- class: "help is-danger",
16091
- style: "display:none"
16092
- },
16093
- ""
16094
- ]));
16095
- break;
16096
- default: log_error(`Unsupported element type, tag ${tag}`);
16174
+ function build_fkey_options(gobj) {
16175
+ let desc = gobj_read_attr(gobj, "desc");
16176
+ let fkey_options = {};
16177
+ for (let col of desc.cols) {
16178
+ if (treedb_get_field_desc(col).type !== "fkey" || !is_object(col.fkey)) continue;
16179
+ let topic_name = Object.keys(col.fkey)[0];
16180
+ if (!topic_name || fkey_options[topic_name] !== void 0) continue;
16181
+ let webix = gobj_command(gobj_parent(gobj), "get_topic_data", { topic_name }, gobj);
16182
+ fkey_options[topic_name] = webix && is_array(webix.data) ? webix.data : [];
16097
16183
  }
16098
- return $element;
16184
+ return fkey_options;
16099
16185
  }
16100
- /******************************************************************
16101
- * Build topic form popup with bulma
16102
- ******************************************************************/
16103
- function build_topic_modal(gobj) {
16104
- const $form = build_topic_form(gobj);
16105
- let title = t(gobj_read_str_attr(gobj, "topic_name"));
16186
+ /************************************************************
16187
+ * Open the edit/create dialog hosting a C_YUI_FORM child.
16188
+ * The child is created fresh on every open (schema and fkey
16189
+ * options are read at its build time) and destroyed on close.
16190
+ * Its EV_SAVE_RECORD (CHILD model) arrives already in treedb
16191
+ * shape see ac_form_save_record().
16192
+ ************************************************************/
16193
+ function open_form_dialog(gobj, mode, record) {
16194
+ close_form_dialog(gobj);
16195
+ let priv = gobj.priv;
16196
+ let desc = gobj_read_attr(gobj, "desc");
16197
+ let popup_id = gobj_read_str_attr(gobj, "popup_id");
16198
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
16106
16199
  let $element = createElement2([
16107
16200
  "div",
16108
16201
  {
16109
- id: gobj_read_str_attr(gobj, "popup_id"),
16110
- class: "modal"
16202
+ id: popup_id,
16203
+ class: "modal is-active"
16111
16204
  },
16112
16205
  [[
16113
16206
  "div",
@@ -16116,333 +16209,120 @@ function build_topic_modal(gobj) {
16116
16209
  ], [
16117
16210
  "div",
16118
16211
  { class: "modal-card modal-is-responsive" },
16119
- [
16120
- [
16121
- "header",
16122
- { class: "modal-card-head p-3" },
16123
- [[
16124
- "p",
16125
- {
16126
- class: "modal-card-title",
16127
- style: "text-align:center;"
16128
- },
16129
- title
16130
- ], ["button", {
16212
+ [[
16213
+ "header",
16214
+ { class: "modal-card-head p-3" },
16215
+ [[
16216
+ "p",
16217
+ {
16218
+ class: "modal-card-title",
16219
+ style: "text-align:center;"
16220
+ },
16221
+ t(topic_name)
16222
+ ], [
16223
+ "button",
16224
+ {
16131
16225
  class: "delete is-large",
16132
16226
  "aria-label": "close"
16133
- }]]
16134
- ],
16135
- [
16136
- "section",
16137
- { class: "modal-card-body p-1" },
16138
- [$form]
16139
- ],
16140
- [
16141
- "footer",
16142
- { class: "modal-card-foot p-3" },
16143
- [[
16144
- "div",
16145
- {
16146
- class: "buttons",
16147
- style: "justify-content:center; width:100%;"
16148
- },
16149
- [
16150
- [
16151
- "button",
16152
- {
16153
- class: "button is-success px-4",
16154
- type: "submit"
16155
- },
16156
- [[
16157
- "span",
16158
- { class: "icon" },
16159
- [["i", { class: "yi-floppy-disk" }]]
16160
- ], [
16161
- "span",
16162
- { i18n: "save" },
16163
- "save"
16164
- ]],
16165
- { click: function(evt) {
16166
- evt.stopPropagation();
16167
- evt.preventDefault();
16168
- let $form = $element.querySelector("form");
16169
- if (validate_form(gobj, $form)) {
16170
- let values = get_form_values($form);
16171
- let ret;
16172
- if ($form.__mode__ === "create") ret = gobj_send_event(gobj, "EV_CREATE_RECORD", values, gobj);
16173
- else ret = gobj_send_event(gobj, "EV_UPDATE_RECORD", values, gobj);
16174
- if (ret === 0) $element.classList.remove("is-active");
16175
- }
16176
- } }
16177
- ],
16178
- [
16179
- "button",
16180
- { class: "button px-4 form-close" },
16181
- [[
16182
- "span",
16183
- { class: "icon" },
16184
- [["i", { class: "yi-xmark" }]]
16185
- ], [
16186
- "span",
16187
- { i18n: "cancel" },
16188
- "cancel"
16189
- ]]
16190
- ],
16191
- [
16192
- "button",
16193
- { class: "button px-4" },
16194
- [[
16195
- "span",
16196
- { class: "icon" },
16197
- [["i", { class: "yi-broom" }]]
16198
- ], [
16199
- "span",
16200
- { i18n: "clear" },
16201
- "clear"
16202
- ]],
16203
- { click: function(evt) {
16204
- evt.stopPropagation();
16205
- clear_validation(gobj, $form);
16206
- clear_data(gobj, $form);
16207
- } }
16208
- ]
16209
- ]
16210
- ]]
16211
- ]
16212
- ]
16227
+ },
16228
+ "",
16229
+ { click: function(evt) {
16230
+ evt.stopPropagation();
16231
+ request_close_form_dialog(gobj);
16232
+ } }
16233
+ ]]
16234
+ ], [
16235
+ "section",
16236
+ {
16237
+ class: "modal-card-body p-2",
16238
+ style: "display:flex; flex-direction:column;"
16239
+ },
16240
+ []
16241
+ ]]
16213
16242
  ]],
16214
16243
  { click: function(evt) {
16215
16244
  evt.stopPropagation();
16216
16245
  } }
16217
16246
  ]);
16218
- $element.querySelectorAll(".modal-close, .form-close, .delete").forEach(function(element) {
16219
- element.addEventListener("click", function(event) {
16220
- event.stopPropagation();
16221
- $element.classList.remove("is-active");
16222
- });
16223
- });
16224
- refresh_language($element, t);
16225
- popup_mount_layer().appendChild($element);
16226
- $element.querySelectorAll(".jsoneditor").forEach(function(element) {
16227
- let font_family = "DejaVu Sans Mono, monospace";
16228
- element.style.setProperty("--jse-font-size-mono", "16px");
16229
- element.style.setProperty("--jse-font-family-mono", font_family);
16230
- element.jsoneditor = new JSONEditor({
16231
- target: element,
16232
- props: { timestampTag: function({ field, value, path }) {
16233
- 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";
16234
- } }
16235
- });
16236
- });
16237
- $element.querySelectorAll(".select2-multiple").forEach((el) => {
16238
- if (el.tomselect) return;
16239
- new TomSelect(el, {
16240
- plugins: ["remove_button"],
16241
- create: false,
16242
- hideSelected: true,
16243
- closeAfterSelect: false
16244
- });
16245
- });
16246
- return $element;
16247
- }
16248
- /************************************************************
16249
- * Show a validation error on a field
16250
- ************************************************************/
16251
- function show_field_error($field, message) {
16252
- $field.querySelectorAll("input, textarea, select").forEach((el) => {
16253
- el.classList.add("is-danger");
16254
- });
16255
- let $help = $field.querySelector(".help");
16256
- if ($help) {
16257
- $help.textContent = message;
16258
- $help.style.display = "block";
16259
- }
16260
- }
16261
- /************************************************************
16262
- * Clear a validation error from a field
16263
- ************************************************************/
16264
- function clear_field_error($field) {
16265
- $field.querySelectorAll("input, textarea, select").forEach((el) => {
16266
- el.classList.remove("is-danger");
16267
- });
16268
- let $help = $field.querySelector(".help");
16269
- if ($help) {
16270
- $help.textContent = "";
16271
- $help.style.display = "none";
16247
+ let $body = $element.querySelector(".modal-card-body");
16248
+ let form = gobj_create_pure_child("form_" + clean_name(gobj_name(gobj)), "C_YUI_FORM", {
16249
+ template: build_form_template(gobj),
16250
+ record,
16251
+ fkey_options: build_fkey_options(gobj),
16252
+ form_mode: mode,
16253
+ pkey: desc.pkey || "id",
16254
+ topic_name,
16255
+ editable: true,
16256
+ $parent: $body
16257
+ }, gobj);
16258
+ priv.form = form;
16259
+ priv.$popup = $element;
16260
+ let $form_container = gobj_read_attr(form, "$container");
16261
+ if ($form_container) {
16262
+ $form_container.style.flex = "1 1 auto";
16263
+ $form_container.style.minHeight = "0";
16272
16264
  }
16265
+ popup_mount_layer().appendChild($element);
16266
+ refresh_language($element, t);
16267
+ gobj_start(form);
16268
+ let $with_focus = $element.querySelector(".with-focus");
16269
+ if ($with_focus) $with_focus.focus();
16273
16270
  }
16274
16271
  /************************************************************
16275
- * Get the current value of a field for validation
16272
+ * Close request from the dialog X: if the form carries
16273
+ * unsaved changes ask first (EV_WINDOW_TO_CLOSE contract).
16276
16274
  ************************************************************/
16277
- function get_field_value_for_validation($field, conf) {
16278
- switch (conf.tag) {
16279
- case "input": {
16280
- let $input = $field.querySelector("input");
16281
- if (!$input) return "";
16282
- if ($input.type === "checkbox") return $input.checked;
16283
- return $input.value;
16284
- }
16285
- case "checkbox": {
16286
- let $input = $field.querySelector("input[type=\"checkbox\"]");
16287
- if (!$input) return false;
16288
- return $input.checked;
16289
- }
16290
- case "textarea": {
16291
- let $el = $field.querySelector("textarea");
16292
- if (!$el) return "";
16293
- return $el.value;
16294
- }
16295
- case "select":
16296
- case "select2": {
16297
- let $el = $field.querySelector("select");
16298
- if (!$el) return "";
16299
- if ($el.tomselect) return $el.tomselect.getValue();
16300
- return $el.value;
16301
- }
16302
- case "jsoneditor": {
16303
- let $el = $field.querySelector(".jsoneditor");
16304
- if (!$el || !$el.jsoneditor) return "";
16305
- let val = $el.jsoneditor.get();
16306
- if (val.text !== void 0) return val.text;
16307
- return JSON.stringify(val.json);
16308
- }
16309
- case "radio": {
16310
- let $checked = $field.querySelector("input[type=\"radio\"]:checked");
16311
- if (!$checked) return "";
16312
- return $checked.value;
16275
+ function request_close_form_dialog(gobj) {
16276
+ let priv = gobj.priv;
16277
+ if (priv.form) {
16278
+ let kw = {};
16279
+ gobj_send_event(priv.form, "EV_WINDOW_TO_CLOSE", kw, gobj);
16280
+ if (kw.abort_close) {
16281
+ get_yesnocancel(t("All changes will be lost. Are you sure?"), function(evt) {
16282
+ if (evt === "yes") close_form_dialog(gobj);
16283
+ });
16284
+ return;
16313
16285
  }
16314
16286
  }
16315
- return "";
16287
+ close_form_dialog(gobj);
16316
16288
  }
16317
16289
  /************************************************************
16318
- * Check if a field value is empty
16290
+ * Destroy the hosted form and remove the dialog DOM.
16319
16291
  ************************************************************/
16320
- function is_field_value_empty(value) {
16321
- if (value === null || value === void 0 || value === "") return true;
16322
- if (Array.isArray(value) && value.length === 0) return true;
16323
- return false;
16324
- }
16325
- /************************************************************
16326
- * Determine if a field requires validation in the current mode
16327
- ************************************************************/
16328
- function is_field_required(conf, mode) {
16329
- if (conf.is_pkey) return mode === "create" && !conf.is_rowid;
16330
- if (conf.readonly) return false;
16331
- return conf.required || conf.not_null;
16332
- }
16333
- /************************************************************
16334
- * Validate a single field on blur
16335
- * Called from input/textarea blur event handlers
16336
- ************************************************************/
16337
- function validate_field_on_blur(element) {
16338
- let $field = element.closest(".xfield");
16339
- if (!$field || !$field.field_conf) return;
16340
- let conf = $field.field_conf;
16341
- let $form = element.closest("form");
16342
- if (!is_field_required(conf, $form ? $form.__mode__ : "update")) {
16343
- clear_field_error($field);
16344
- return;
16292
+ function close_form_dialog(gobj) {
16293
+ let priv = gobj.priv;
16294
+ if (priv.form) {
16295
+ if (gobj_is_running(priv.form)) gobj_stop(priv.form);
16296
+ gobj_destroy(priv.form);
16297
+ priv.form = null;
16345
16298
  }
16346
- if (is_field_value_empty(get_field_value_for_validation($field, conf))) show_field_error($field, t("this field is required"));
16347
- else clear_field_error($field);
16348
- }
16349
- /************************************************************
16350
- * Validate all required fields in the form
16351
- * Returns true if valid, false otherwise
16352
- ************************************************************/
16353
- function validate_form(gobj, $form) {
16354
- let is_valid = true;
16355
- let first_invalid = null;
16356
- let mode = $form.__mode__ || "update";
16357
- $form.querySelectorAll(".xfield").forEach(($field) => {
16358
- let conf = $field.field_conf;
16359
- if (!conf) return;
16360
- if (!is_field_required(conf, mode)) {
16361
- clear_field_error($field);
16362
- return;
16363
- }
16364
- if (is_field_value_empty(get_field_value_for_validation($field, conf))) {
16365
- show_field_error($field, t("this field is required"));
16366
- is_valid = false;
16367
- if (!first_invalid) first_invalid = $field;
16368
- } else clear_field_error($field);
16369
- });
16370
- if (first_invalid) {
16371
- let $focusable = first_invalid.querySelector("input, select, textarea");
16372
- if ($focusable && $focusable.focus) $focusable.focus();
16299
+ if (priv.$popup) {
16300
+ if (priv.$popup.parentNode) priv.$popup.parentNode.removeChild(priv.$popup);
16301
+ priv.$popup = null;
16373
16302
  }
16374
- return is_valid;
16375
- }
16376
- /************************************************************
16377
- * Clear all validation errors from the form
16378
- ************************************************************/
16379
- function clear_validation(gobj, $form) {
16380
- $form.querySelectorAll(".xfield").forEach(($field) => {
16381
- clear_field_error($field);
16382
- });
16383
16303
  }
16384
16304
  /************************************************************
16385
- * Before show edit/create from,
16386
- * set form state "update" or "create"
16387
- * Only the "id" field is modified,
16388
- * in "update" is readonly
16389
- * in "create" is writable
16390
- * Although by default the pkey (primary key) is "id",
16391
- * use always desc.pkey value, who knows all cases!
16305
+ * True if the topic's pkey col carries the "rowid" flag.
16392
16306
  ************************************************************/
16393
- function set_form_mode(gobj, $form, mode, index) {
16307
+ function pkey_is_rowid(gobj) {
16394
16308
  let desc = gobj_read_attr(gobj, "desc");
16395
- let is_rowid = str_in_list(kwid_find_one_record(gobj, desc.cols, null, { "id": desc.pkey }).flag, "rowid");
16396
- let $id = $form.querySelector(`[name="${desc.pkey}"]`);
16397
- if ($id) if (mode === "update") {
16398
- $id.setAttribute("readonly", "readonly");
16399
- $id.removeAttribute("required");
16400
- } else if (is_rowid) {
16401
- $id.setAttribute("readonly", "readonly");
16402
- $id.removeAttribute("required");
16403
- } else {
16404
- $id.removeAttribute("readonly");
16405
- $id.setAttribute("required", "");
16406
- }
16407
- else log_error(sprintf("%s: form without id field ('%s')", gobj_short_name(gobj), desc.pkey));
16408
- if (is_rowid) mode = "create";
16409
- $form.__mode__ = mode;
16410
- $form.__index__ = index;
16309
+ let col_pkey = kwid_find_one_record(gobj, desc.cols, null, { "id": desc.pkey });
16310
+ if (!col_pkey) return false;
16311
+ return str_in_list(col_pkey.flag, "rowid");
16411
16312
  }
16412
16313
  /************************************************************
16413
16314
  * Show the edit "update" form of a row (record)
16414
16315
  * internally called from the icon edit at Op column
16415
16316
  ************************************************************/
16416
16317
  function show_edit_form(gobj, row, index) {
16417
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
16418
- let form_id = gobj_read_str_attr(gobj, "form_id");
16419
- let $element = document.getElementById(popup_id);
16420
- if (!$element) $element = build_topic_modal(gobj);
16421
- let $form = document.getElementById(form_id);
16422
- clear_validation(gobj, $form);
16423
- if (json_size(row) === 0 || json_size(row) === 1 && "_check_box_state_" in row) set_form_mode(gobj, $form, "create", index);
16424
- else set_form_mode(gobj, $form, "update", index);
16425
- set_form_values(gobj, $form, row);
16426
- $element.classList.add("is-active");
16427
- let $with_focus = $element.querySelector(".with-focus");
16428
- if ($with_focus) $with_focus.focus();
16318
+ open_form_dialog(gobj, pkey_is_rowid(gobj) ? "create" : "update", row);
16429
16319
  }
16430
16320
  /************************************************************
16431
16321
  * Show the edit "create" form to a new record
16432
16322
  * internally called from the top toolbar +New button
16433
16323
  ************************************************************/
16434
16324
  function show_create_form(gobj, row) {
16435
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
16436
- let form_id = gobj_read_str_attr(gobj, "form_id");
16437
- let $element = document.getElementById(popup_id);
16438
- if (!$element) $element = build_topic_modal(gobj);
16439
- let $form = document.getElementById(form_id);
16440
- clear_validation(gobj, $form);
16441
- set_form_mode(gobj, $form, "create", -1);
16442
- set_form_values(gobj, $form, row);
16443
- $element.classList.add("is-active");
16444
- let $with_focus = $element.querySelector(".with-focus");
16445
- if ($with_focus) $with_focus.focus();
16325
+ open_form_dialog(gobj, "create", row);
16446
16326
  }
16447
16327
  /************************************************************
16448
16328
  * Return column schema of field id
@@ -16456,174 +16336,6 @@ function get_schema_col(gobj, id) {
16456
16336
  return null;
16457
16337
  }
16458
16338
  /************************************************************
16459
- *
16460
- ************************************************************/
16461
- function clear_data(gobj, $form) {
16462
- $form.querySelectorAll("input, select, textarea, .jsoneditor").forEach((input) => {
16463
- if (input.name !== "id") if (input.classList.contains("jsoneditor")) {
16464
- let jsoneditor = input.jsoneditor;
16465
- if (jsoneditor) jsoneditor.set({ text: "" });
16466
- } else if (input.type === "checkbox") input.checked = false;
16467
- else if (input.type === "radio") input.checked = false;
16468
- else if (input.type === "datetime-local") input.value = null;
16469
- else if (input.multiple) {
16470
- if (input.tomselect) input.tomselect.clear();
16471
- } else input.value = null;
16472
- });
16473
- }
16474
- /************************************************************
16475
- *
16476
- ************************************************************/
16477
- function get_form_values($form) {
16478
- const values = {};
16479
- $form.querySelectorAll("input, select, textarea, .jsoneditor").forEach((input) => {
16480
- if (input.classList.contains("jsoneditor")) {
16481
- let jsoneditor = input.jsoneditor;
16482
- if (jsoneditor) {
16483
- let name = input.getAttribute("name");
16484
- let value = jsoneditor.get();
16485
- if (value.text === void 0) values[name] = JSON.stringify(value.json, null, null);
16486
- else values[name] = value.text;
16487
- }
16488
- } else if (input.type === "checkbox") values[input.name] = input.checked;
16489
- else if (input.type === "radio") {
16490
- if (input.checked) values[input.name] = input.value;
16491
- } else if (input.type === "datetime-local") {
16492
- const date = new Date(input.value);
16493
- const epochTime = Math.floor(date.getTime() / 1e3);
16494
- values[input.name] = epochTime;
16495
- } else if (input.multiple) {
16496
- if (input.tomselect) values[input.name] = input.tomselect.getValue();
16497
- } else values[input.name] = input.value;
16498
- });
16499
- return values;
16500
- }
16501
- /************************************************************
16502
- * Convert a record column value from backend to frontend
16503
- ************************************************************/
16504
- function epochToDateTimeLocal(epochTime) {
16505
- const date = /* @__PURE__ */ new Date(epochTime * 1e3);
16506
- const year = date.getFullYear();
16507
- const month = String(date.getMonth() + 1).padStart(2, "0");
16508
- const day = String(date.getDate()).padStart(2, "0");
16509
- const hours = String(date.getHours()).padStart(2, "0");
16510
- const minutes = String(date.getMinutes()).padStart(2, "0");
16511
- String(date.getSeconds()).padStart(2, "0");
16512
- return `${year}-${month}-${day}T${hours}:${minutes}`;
16513
- }
16514
- /************************************************************
16515
- *
16516
- ************************************************************/
16517
- function set_form_values(gobj, form, values) {
16518
- Object.keys(values).forEach((key) => {
16519
- let element = form.querySelector(`[name="${key}"]`);
16520
- if (element) {
16521
- let value = values[key];
16522
- let col = get_schema_col(gobj, key);
16523
- if (col) value = transform__treedb_value_2_form_value(gobj, col, value);
16524
- switch (element.type) {
16525
- case "select":
16526
- element.value = value;
16527
- break;
16528
- case "select-one":
16529
- element.value = value;
16530
- break;
16531
- case "select-multiple":
16532
- if (element.tomselect) element.tomselect.setValue(value);
16533
- break;
16534
- case "radio":
16535
- let radio = form.querySelector(`input[name="${key}"][value="${values[key]}"]`);
16536
- if (radio) radio.checked = true;
16537
- break;
16538
- case "checkbox":
16539
- element.checked = value;
16540
- break;
16541
- case "text":
16542
- case "textarea":
16543
- case "email":
16544
- element.value = value;
16545
- break;
16546
- case "datetime-local":
16547
- element.value = epochToDateTimeLocal(value);
16548
- break;
16549
- default:
16550
- if (element.classList.contains("jsoneditor")) {
16551
- let jsoneditor = element.jsoneditor;
16552
- if (jsoneditor) jsoneditor.set(value);
16553
- break;
16554
- }
16555
- element.value = value;
16556
- log_error(sprintf("%s: form type unknown: '%s'", gobj_short_name(gobj), element.type));
16557
- break;
16558
- }
16559
- }
16560
- });
16561
- }
16562
- /************************************************************
16563
- * Convert a record column value from backend
16564
- * to frontend form field
16565
- ************************************************************/
16566
- function transform__treedb_value_2_form_value(gobj, col, value) {
16567
- const field_desc = treedb_get_field_desc(col);
16568
- switch (field_desc.type) {
16569
- case "string":
16570
- case "email":
16571
- case "tel":
16572
- case "url": break;
16573
- case "integer": break;
16574
- case "real": break;
16575
- case "boolean": break;
16576
- case "object":
16577
- case "dict":
16578
- case "template":
16579
- case "array":
16580
- case "list":
16581
- case "coordinates":
16582
- case "blob":
16583
- value = { json: value };
16584
- break;
16585
- case "enum":
16586
- switch (field_desc.real_type) {
16587
- case "string": break;
16588
- case "object":
16589
- case "dict":
16590
- case "array":
16591
- case "list": break;
16592
- }
16593
- break;
16594
- case "now":
16595
- case "time":
16596
- switch (field_desc.real_type) {
16597
- case "string":
16598
- case "integer":
16599
- value = parseInt(value) || 0;
16600
- value = new Date(value);
16601
- break;
16602
- }
16603
- break;
16604
- case "color":
16605
- switch (field_desc.real_type) {
16606
- case "string": break;
16607
- case "integer": break;
16608
- }
16609
- break;
16610
- case "hook": break;
16611
- case "fkey":
16612
- let new_value = [];
16613
- if (value) if (is_string(value)) {
16614
- let fkey = treedb_decoder_fkey(col, value);
16615
- if (fkey) new_value.push(fkey.id);
16616
- } else if (is_array(value)) for (let i = 0; i < value.length; i++) {
16617
- let fkey = treedb_decoder_fkey(col, value[i]);
16618
- if (fkey) new_value.push(fkey.id);
16619
- }
16620
- else log_error("fkey type unsupported: " + JSON.stringify(value));
16621
- value = new_value;
16622
- break;
16623
- }
16624
- return value;
16625
- }
16626
- /************************************************************
16627
16339
  * Convert from frontend to backend
16628
16340
  * operation: "create" "update"
16629
16341
  ************************************************************/
@@ -16747,18 +16459,6 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation) {
16747
16459
  /************************************************************
16748
16460
  *
16749
16461
  ************************************************************/
16750
- function get_fkey_options(gobj, col) {
16751
- for (let topic_name of Object.keys(col.fkey)) {
16752
- let kw = { topic_name };
16753
- let current_list = gobj_command(gobj_parent(gobj), "get_topic_data", kw, gobj).data;
16754
- return list2options(current_list, "id", "id");
16755
- }
16756
- log_error("No fkey options found" + JSON.stringify(col));
16757
- return null;
16758
- }
16759
- /************************************************************
16760
- *
16761
- ************************************************************/
16762
16462
  function fkey_in_col(col, topic_name, hook_name) {
16763
16463
  if (hook_name === col.fkey[topic_name]) return true;
16764
16464
  return false;
@@ -17088,37 +16788,18 @@ function ac_paste_rows(gobj, event, kw, src) {
17088
16788
  return 0;
17089
16789
  }
17090
16790
  /************************************************************
17091
- * From internal Save button of "create" modal form
17092
- ************************************************************/
17093
- function ac_create_record(gobj, event, kw, src) {
17094
- let new_kw = null;
17095
- try {
17096
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "create");
17097
- } catch (e) {
17098
- log_warning(e);
17099
- throw e;
17100
- }
17101
- gobj_publish_event(gobj, "EV_CREATE_RECORD", {
17102
- topic_name: gobj_read_str_attr(gobj, "topic_name"),
17103
- record: new_kw
17104
- });
17105
- return 0;
17106
- }
17107
- /************************************************************
17108
- * From internal Save button of "update" modal form
16791
+ * EV_SAVE_RECORD from the hosted C_YUI_FORM: kw is the
16792
+ * record already in treedb shape (the form itself encodes
16793
+ * fkeys, times and numbers). Route by the form's mode.
17109
16794
  ************************************************************/
17110
- function ac_update_record(gobj, event, kw, src) {
17111
- let new_kw = null;
17112
- try {
17113
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "update");
17114
- } catch (e) {
17115
- log_warning(e);
17116
- throw e;
17117
- }
17118
- gobj_publish_event(gobj, "EV_UPDATE_RECORD", {
16795
+ function ac_form_save_record(gobj, event, kw, src) {
16796
+ gobj_publish_event(gobj, gobj_read_str_attr(src, "form_mode") === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
17119
16797
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
17120
- record: new_kw
16798
+ record: kw
17121
16799
  });
16800
+ setTimeout(function() {
16801
+ close_form_dialog(gobj);
16802
+ }, 0);
17122
16803
  return 0;
17123
16804
  }
17124
16805
  /************************************************************
@@ -17248,13 +16929,8 @@ function create_gclass$2(gclass_name) {
17248
16929
  null
17249
16930
  ],
17250
16931
  [
17251
- "EV_CREATE_RECORD",
17252
- ac_create_record,
17253
- null
17254
- ],
17255
- [
17256
- "EV_UPDATE_RECORD",
17257
- ac_update_record,
16932
+ "EV_SAVE_RECORD",
16933
+ ac_form_save_record,
17258
16934
  null
17259
16935
  ],
17260
16936
  [
@@ -17319,6 +16995,7 @@ function create_gclass$2(gclass_name) {
17319
16995
  ["EV_LOAD_NODE_UPDATED", 0],
17320
16996
  ["EV_NODE_DELETED", 0],
17321
16997
  ["EV_EDITION_MODE", 0],
16998
+ ["EV_SAVE_RECORD", 0],
17322
16999
  ["EV_NEW_ROW", 0],
17323
17000
  ["EV_DELETE_ROWS", 0],
17324
17001
  ["EV_COPY_ROWS", 0],
@@ -17342,6 +17019,7 @@ function create_gclass$2(gclass_name) {
17342
17019
  * Register GClass
17343
17020
  ***************************************************************/
17344
17021
  function register_c_yui_treedb_topic_with_form() {
17022
+ if (!gclass_find_by_name("C_YUI_FORM", false)) register_c_yui_form();
17345
17023
  return create_gclass$2(GCLASS_NAME$2);
17346
17024
  }
17347
17025
  //#endregion