@yuneta/gobj-ui 7.7.0 → 7.8.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.
@@ -7493,13 +7493,8 @@ function field_validation_message($input) {
7493
7493
  ************************************************************/
7494
7494
  function mark_field_validity($input) {
7495
7495
  let $h = $input.parentNode ? $input.parentNode.querySelector(".help") : null;
7496
- let ok = true;
7497
- try {
7498
- ok = $input.checkValidity();
7499
- } catch (e) {
7500
- ok = true;
7501
- }
7502
- if (ok) {
7496
+ let v = $input.validity;
7497
+ if (!v || v.valid) {
7503
7498
  $input.classList.remove("is-danger");
7504
7499
  if ($h) {
7505
7500
  $h.textContent = "";
@@ -15614,7 +15609,13 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15614
15609
  }
15615
15610
  if (result < 0) {
15616
15611
  if (command === "descs") show_load_error$1(gobj, (0, i18next.t)(comment));
15617
- else yui_shell_show_error(yui_shell_of(gobj), comment, { t: i18next.t });
15612
+ else {
15613
+ yui_shell_show_error(yui_shell_of(gobj), comment, { t: i18next.t });
15614
+ if (command === "update-node" || command === "create-node") {
15615
+ let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
15616
+ if (failed_topic) treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), failed_topic, { list_dict: true });
15617
+ }
15618
+ }
15618
15619
  return 0;
15619
15620
  }
15620
15621
  switch (command) {
@@ -15854,6 +15855,35 @@ function ac_create_record(gobj, event, kw, src) {
15854
15855
  });
15855
15856
  }
15856
15857
  /********************************************
15858
+ * Event from formtable: ONE field of one record, edited in place.
15859
+ *
15860
+ * Written as a PARTIAL update and, deliberately, with NO `autolink`.
15861
+ * `treedb_update_node()` merges (`json_object_update`), so the fields
15862
+ * it does not carry are left alone; and `autolink` is what wipes a
15863
+ * node's links to rebuild them from the fkeys the record carries — on
15864
+ * a partial record that reads as "no parents" and DETACHES the node,
15865
+ * answering success. A cell edit changes a scalar and must not be able
15866
+ * to touch a link at all.
15867
+ *
15868
+ * This is why it is its own event and not EV_UPDATE_RECORD: that one
15869
+ * DOES send autolink, and may, because the form hands it the whole
15870
+ * record with its fkeys in it.
15871
+ ********************************************/
15872
+ function ac_update_field(gobj, event, kw, src) {
15873
+ if (refuse_if_readonly$3(gobj, event)) return -1;
15874
+ let topic_name = kw.topic_name || (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15875
+ let desc = ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "descs") || {})[topic_name];
15876
+ let pkey = desc && desc.pkey || "id";
15877
+ if (kw.id === void 0 || kw.id === null || !kw.field) {
15878
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a field write with no id or no field`);
15879
+ return -1;
15880
+ }
15881
+ let record = {};
15882
+ record[pkey] = kw.id;
15883
+ record[kw.field] = kw.value;
15884
+ return treedb_update_node$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), topic_name, record, { list_dict: true });
15885
+ }
15886
+ /********************************************
15857
15887
  * Event from formtable
15858
15888
  * kw: {
15859
15889
  * topic_name,
@@ -15975,6 +16005,11 @@ function create_gclass$6(gclass_name) {
15975
16005
  ac_update_record,
15976
16006
  null
15977
16007
  ],
16008
+ [
16009
+ "EV_UPDATE_FIELD",
16010
+ ac_update_field,
16011
+ null
16012
+ ],
15978
16013
  [
15979
16014
  "EV_DELETE_RECORD",
15980
16015
  ac_delete_record,
@@ -16048,6 +16083,7 @@ function create_gclass$6(gclass_name) {
16048
16083
  ["EV_TREEDB_NODE_DELETED", _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],
16049
16084
  ["EV_CREATE_RECORD", 0],
16050
16085
  ["EV_UPDATE_RECORD", 0],
16086
+ ["EV_UPDATE_FIELD", 0],
16051
16087
  ["EV_DELETE_RECORD", 0],
16052
16088
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
16053
16089
  ["EV_REFRESH_TOPIC", 0],
@@ -16261,6 +16297,7 @@ var attrs_table$5 = [
16261
16297
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
16262
16298
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16263
16299
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
16300
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
16264
16301
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16265
16302
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16266
16303
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16770,6 +16807,7 @@ function create_tabulator(gobj) {
16770
16807
  let table_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "table_id");
16771
16808
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
16772
16809
  let with_header_filters = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_header_filters");
16810
+ let with_inline_edit = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_inline_edit");
16773
16811
  let columns = [];
16774
16812
  let with_checkbox = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_checkbox");
16775
16813
  let with_radio = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_radio");
@@ -16813,6 +16851,42 @@ function create_tabulator(gobj) {
16813
16851
  col_id: cell.getField()
16814
16852
  }, gobj);
16815
16853
  }
16854
+ const INLINE_EDITABLE = [
16855
+ "string",
16856
+ "integer",
16857
+ "real",
16858
+ "email",
16859
+ "url",
16860
+ "tel",
16861
+ "id",
16862
+ "hex",
16863
+ "currency",
16864
+ "percent",
16865
+ "uuid"
16866
+ ];
16867
+ function inline_editor_for(colDef, col, field_desc) {
16868
+ if (!field_desc.is_writable) return;
16869
+ if (col.id === (desc.pkey || "id")) return;
16870
+ switch (field_desc.type) {
16871
+ case "boolean":
16872
+ colDef.editor = "tickCross";
16873
+ break;
16874
+ case "enum":
16875
+ colDef.editor = "list";
16876
+ colDef.editorParams = { values: field_desc.enum_list || [] };
16877
+ break;
16878
+ case "integer":
16879
+ case "real":
16880
+ colDef.editor = "number";
16881
+ break;
16882
+ default:
16883
+ if (!INLINE_EDITABLE.includes(field_desc.type)) return;
16884
+ colDef.editor = "input";
16885
+ }
16886
+ colDef.editable = function() {
16887
+ return (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable") && !(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly");
16888
+ };
16889
+ }
16816
16890
  const FILTERABLE = [
16817
16891
  "string",
16818
16892
  "integer",
@@ -16935,6 +17009,7 @@ function create_tabulator(gobj) {
16935
17009
  formatter: colFormatter
16936
17010
  };
16937
17011
  if (with_header_filters) apply_header_filter(colDef, field_desc);
17012
+ if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
16938
17013
  if (vertAlign) colDef.vertAlign = vertAlign;
16939
17014
  if (formatterParams) colDef.formatterParams = formatterParams;
16940
17015
  if (cellClick) colDef.cellClick = cellClick;
@@ -17021,6 +17096,21 @@ function create_tabulator(gobj) {
17021
17096
  });
17022
17097
  tabulator.on("dataProcessed", update_rowcount);
17023
17098
  tabulator.on("dataChanged", update_rowcount);
17099
+ tabulator.on("cellEdited", function(cell) {
17100
+ let pkey = ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc") || {}).pkey || "id";
17101
+ let data = cell.getRow().getData();
17102
+ let id = data ? data[pkey] : void 0;
17103
+ if (id === void 0 || id === null) {
17104
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: edited a row with no '${pkey}'`);
17105
+ cell.restoreOldValue();
17106
+ return;
17107
+ }
17108
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CELL_EDITED", {
17109
+ id,
17110
+ field: cell.getField(),
17111
+ value: cell.getValue()
17112
+ }, gobj);
17113
+ });
17024
17114
  tabulator.on("dataFiltered", function(filters, rows) {
17025
17115
  update_rowcount(Array.isArray(rows) ? rows.length : void 0);
17026
17116
  });
@@ -18141,6 +18231,20 @@ function ac_show_cell_json(gobj, event, kw, src) {
18141
18231
  return 0;
18142
18232
  }
18143
18233
  /************************************************************
18234
+ * One cell of one record changed. Publish it UP as a field write;
18235
+ * the host owns the treedb command and its options.
18236
+ ************************************************************/
18237
+ function ac_cell_edited(gobj, event, kw, src) {
18238
+ if (refuse_if_readonly$2(gobj, event)) return -1;
18239
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_UPDATE_FIELD", {
18240
+ topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
18241
+ id: kw.id,
18242
+ field: kw.field,
18243
+ value: kw.value
18244
+ });
18245
+ return 0;
18246
+ }
18247
+ /************************************************************
18144
18248
  * Show the schema (desc) of this topic
18145
18249
  ************************************************************/
18146
18250
  function ac_show_schema(gobj, event, kw, src) {
@@ -18390,6 +18494,11 @@ function create_gclass$5(gclass_name) {
18390
18494
  ac_show_schema,
18391
18495
  null
18392
18496
  ],
18497
+ [
18498
+ "EV_CELL_EDITED",
18499
+ ac_cell_edited,
18500
+ null
18501
+ ],
18393
18502
  [
18394
18503
  "EV_SEARCH",
18395
18504
  ac_search,
@@ -18440,6 +18549,8 @@ function create_gclass$5(gclass_name) {
18440
18549
  ["EV_CHANGE_LOCALE", 0],
18441
18550
  ["EV_REFRESH", 0],
18442
18551
  ["EV_SHOW_SCHEMA", 0],
18552
+ ["EV_CELL_EDITED", 0],
18553
+ ["EV_UPDATE_FIELD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18443
18554
  ["EV_SEARCH", 0],
18444
18555
  ["EV_OPEN_COLUMNS", 0],
18445
18556
  ["EV_TOGGLE_COLUMN", 0],
@@ -7488,13 +7488,8 @@ function field_validation_message($input) {
7488
7488
  ************************************************************/
7489
7489
  function mark_field_validity($input) {
7490
7490
  let $h = $input.parentNode ? $input.parentNode.querySelector(".help") : null;
7491
- let ok = true;
7492
- try {
7493
- ok = $input.checkValidity();
7494
- } catch (e) {
7495
- ok = true;
7496
- }
7497
- if (ok) {
7491
+ let v = $input.validity;
7492
+ if (!v || v.valid) {
7498
7493
  $input.classList.remove("is-danger");
7499
7494
  if ($h) {
7500
7495
  $h.textContent = "";
@@ -15609,7 +15604,13 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15609
15604
  }
15610
15605
  if (result < 0) {
15611
15606
  if (command === "descs") show_load_error$1(gobj, t(comment));
15612
- else yui_shell_show_error(yui_shell_of(gobj), comment, { t });
15607
+ else {
15608
+ yui_shell_show_error(yui_shell_of(gobj), comment, { t });
15609
+ if (command === "update-node" || command === "create-node") {
15610
+ let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
15611
+ if (failed_topic) treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), failed_topic, { list_dict: true });
15612
+ }
15613
+ }
15613
15614
  return 0;
15614
15615
  }
15615
15616
  switch (command) {
@@ -15849,6 +15850,35 @@ function ac_create_record(gobj, event, kw, src) {
15849
15850
  });
15850
15851
  }
15851
15852
  /********************************************
15853
+ * Event from formtable: ONE field of one record, edited in place.
15854
+ *
15855
+ * Written as a PARTIAL update and, deliberately, with NO `autolink`.
15856
+ * `treedb_update_node()` merges (`json_object_update`), so the fields
15857
+ * it does not carry are left alone; and `autolink` is what wipes a
15858
+ * node's links to rebuild them from the fkeys the record carries — on
15859
+ * a partial record that reads as "no parents" and DETACHES the node,
15860
+ * answering success. A cell edit changes a scalar and must not be able
15861
+ * to touch a link at all.
15862
+ *
15863
+ * This is why it is its own event and not EV_UPDATE_RECORD: that one
15864
+ * DOES send autolink, and may, because the form hands it the whole
15865
+ * record with its fkeys in it.
15866
+ ********************************************/
15867
+ function ac_update_field(gobj, event, kw, src) {
15868
+ if (refuse_if_readonly$3(gobj, event)) return -1;
15869
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
15870
+ let desc = (gobj_read_attr(gobj, "descs") || {})[topic_name];
15871
+ let pkey = desc && desc.pkey || "id";
15872
+ if (kw.id === void 0 || kw.id === null || !kw.field) {
15873
+ log_error(`${gobj_short_name(gobj)}: a field write with no id or no field`);
15874
+ return -1;
15875
+ }
15876
+ let record = {};
15877
+ record[pkey] = kw.id;
15878
+ record[kw.field] = kw.value;
15879
+ return treedb_update_node$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), topic_name, record, { list_dict: true });
15880
+ }
15881
+ /********************************************
15852
15882
  * Event from formtable
15853
15883
  * kw: {
15854
15884
  * topic_name,
@@ -15970,6 +16000,11 @@ function create_gclass$6(gclass_name) {
15970
16000
  ac_update_record,
15971
16001
  null
15972
16002
  ],
16003
+ [
16004
+ "EV_UPDATE_FIELD",
16005
+ ac_update_field,
16006
+ null
16007
+ ],
15973
16008
  [
15974
16009
  "EV_DELETE_RECORD",
15975
16010
  ac_delete_record,
@@ -16043,6 +16078,7 @@ function create_gclass$6(gclass_name) {
16043
16078
  ["EV_TREEDB_NODE_DELETED", event_flag_t.EVF_PUBLIC_EVENT],
16044
16079
  ["EV_CREATE_RECORD", 0],
16045
16080
  ["EV_UPDATE_RECORD", 0],
16081
+ ["EV_UPDATE_FIELD", 0],
16046
16082
  ["EV_DELETE_RECORD", 0],
16047
16083
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
16048
16084
  ["EV_REFRESH_TOPIC", 0],
@@ -16256,6 +16292,7 @@ var attrs_table$5 = [
16256
16292
  SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
16257
16293
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16258
16294
  SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
16295
+ SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
16259
16296
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16260
16297
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16261
16298
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16765,6 +16802,7 @@ function create_tabulator(gobj) {
16765
16802
  let table_id = gobj_read_str_attr(gobj, "table_id");
16766
16803
  let desc = gobj_read_attr(gobj, "desc");
16767
16804
  let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
16805
+ let with_inline_edit = gobj_read_bool_attr(gobj, "with_inline_edit");
16768
16806
  let columns = [];
16769
16807
  let with_checkbox = gobj_read_bool_attr(gobj, "with_checkbox");
16770
16808
  let with_radio = gobj_read_bool_attr(gobj, "with_radio");
@@ -16808,6 +16846,42 @@ function create_tabulator(gobj) {
16808
16846
  col_id: cell.getField()
16809
16847
  }, gobj);
16810
16848
  }
16849
+ const INLINE_EDITABLE = [
16850
+ "string",
16851
+ "integer",
16852
+ "real",
16853
+ "email",
16854
+ "url",
16855
+ "tel",
16856
+ "id",
16857
+ "hex",
16858
+ "currency",
16859
+ "percent",
16860
+ "uuid"
16861
+ ];
16862
+ function inline_editor_for(colDef, col, field_desc) {
16863
+ if (!field_desc.is_writable) return;
16864
+ if (col.id === (desc.pkey || "id")) return;
16865
+ switch (field_desc.type) {
16866
+ case "boolean":
16867
+ colDef.editor = "tickCross";
16868
+ break;
16869
+ case "enum":
16870
+ colDef.editor = "list";
16871
+ colDef.editorParams = { values: field_desc.enum_list || [] };
16872
+ break;
16873
+ case "integer":
16874
+ case "real":
16875
+ colDef.editor = "number";
16876
+ break;
16877
+ default:
16878
+ if (!INLINE_EDITABLE.includes(field_desc.type)) return;
16879
+ colDef.editor = "input";
16880
+ }
16881
+ colDef.editable = function() {
16882
+ return gobj_read_bool_attr(gobj, "editable") && !gobj_read_bool_attr(gobj, "readonly");
16883
+ };
16884
+ }
16811
16885
  const FILTERABLE = [
16812
16886
  "string",
16813
16887
  "integer",
@@ -16930,6 +17004,7 @@ function create_tabulator(gobj) {
16930
17004
  formatter: colFormatter
16931
17005
  };
16932
17006
  if (with_header_filters) apply_header_filter(colDef, field_desc);
17007
+ if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
16933
17008
  if (vertAlign) colDef.vertAlign = vertAlign;
16934
17009
  if (formatterParams) colDef.formatterParams = formatterParams;
16935
17010
  if (cellClick) colDef.cellClick = cellClick;
@@ -17016,6 +17091,21 @@ function create_tabulator(gobj) {
17016
17091
  });
17017
17092
  tabulator.on("dataProcessed", update_rowcount);
17018
17093
  tabulator.on("dataChanged", update_rowcount);
17094
+ tabulator.on("cellEdited", function(cell) {
17095
+ let pkey = (gobj_read_attr(gobj, "desc") || {}).pkey || "id";
17096
+ let data = cell.getRow().getData();
17097
+ let id = data ? data[pkey] : void 0;
17098
+ if (id === void 0 || id === null) {
17099
+ log_error(`${gobj_short_name(gobj)}: edited a row with no '${pkey}'`);
17100
+ cell.restoreOldValue();
17101
+ return;
17102
+ }
17103
+ gobj_send_event(gobj, "EV_CELL_EDITED", {
17104
+ id,
17105
+ field: cell.getField(),
17106
+ value: cell.getValue()
17107
+ }, gobj);
17108
+ });
17019
17109
  tabulator.on("dataFiltered", function(filters, rows) {
17020
17110
  update_rowcount(Array.isArray(rows) ? rows.length : void 0);
17021
17111
  });
@@ -18136,6 +18226,20 @@ function ac_show_cell_json(gobj, event, kw, src) {
18136
18226
  return 0;
18137
18227
  }
18138
18228
  /************************************************************
18229
+ * One cell of one record changed. Publish it UP as a field write;
18230
+ * the host owns the treedb command and its options.
18231
+ ************************************************************/
18232
+ function ac_cell_edited(gobj, event, kw, src) {
18233
+ if (refuse_if_readonly$2(gobj, event)) return -1;
18234
+ gobj_publish_event(gobj, "EV_UPDATE_FIELD", {
18235
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
18236
+ id: kw.id,
18237
+ field: kw.field,
18238
+ value: kw.value
18239
+ });
18240
+ return 0;
18241
+ }
18242
+ /************************************************************
18139
18243
  * Show the schema (desc) of this topic
18140
18244
  ************************************************************/
18141
18245
  function ac_show_schema(gobj, event, kw, src) {
@@ -18385,6 +18489,11 @@ function create_gclass$5(gclass_name) {
18385
18489
  ac_show_schema,
18386
18490
  null
18387
18491
  ],
18492
+ [
18493
+ "EV_CELL_EDITED",
18494
+ ac_cell_edited,
18495
+ null
18496
+ ],
18388
18497
  [
18389
18498
  "EV_SEARCH",
18390
18499
  ac_search,
@@ -18435,6 +18544,8 @@ function create_gclass$5(gclass_name) {
18435
18544
  ["EV_CHANGE_LOCALE", 0],
18436
18545
  ["EV_REFRESH", 0],
18437
18546
  ["EV_SHOW_SCHEMA", 0],
18547
+ ["EV_CELL_EDITED", 0],
18548
+ ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
18438
18549
  ["EV_SEARCH", 0],
18439
18550
  ["EV_OPEN_COLUMNS", 0],
18440
18551
  ["EV_TOGGLE_COLUMN", 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.7.0",
3
+ "version": "7.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
package/src/c_yui_form.js CHANGED
@@ -1990,12 +1990,16 @@ function field_validation_message($input)
1990
1990
  function mark_field_validity($input)
1991
1991
  {
1992
1992
  let $h = $input.parentNode? $input.parentNode.querySelector('.help') : null;
1993
- let ok = true;
1994
- try {
1995
- ok = $input.checkValidity();
1996
- } catch(e) {
1997
- ok = true; /* not a validatable control */
1998
- }
1993
+
1994
+ /* `validity.valid`, NEVER `checkValidity()`.
1995
+ *
1996
+ * checkValidity() FIRES the `invalid` event, and this function is what
1997
+ * that event's handler calls: one asks the other for ever. It cost a
1998
+ * page full of "too much recursion" the first time Save reported a bad
1999
+ * field. Reading `validity` asks the same question and fires nothing.
2000
+ * A control with no `validity` (a wrapper div) is not invalid. */
2001
+ let v = $input.validity;
2002
+ let ok = !v || v.valid;
1999
2003
 
2000
2004
  if(ok) {
2001
2005
  $input.classList.remove('is-danger');
@@ -111,6 +111,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button
111
111
  SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
112
112
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
113
113
  SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
114
+ SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
114
115
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
115
116
 
116
117
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
@@ -730,6 +731,7 @@ function create_tabulator(gobj)
730
731
  let table_id = gobj_read_str_attr(gobj, "table_id");
731
732
  let desc = gobj_read_attr(gobj, "desc");
732
733
  let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
734
+ let with_inline_edit = gobj_read_bool_attr(gobj, "with_inline_edit");
733
735
 
734
736
  let columns = [];
735
737
 
@@ -819,6 +821,62 @@ function create_tabulator(gobj)
819
821
  * value is stringified first because a fkey can arrive as a ref string,
820
822
  * a list of them or a dict.
821
823
  */
824
+ /* A cell you can type into, and the ones you cannot.
825
+ *
826
+ * Editing in place is for a SCALAR the table can round-trip. A hook
827
+ * holds children and an fkey IS a link — both are edited by linking,
828
+ * not by typing — a dict or a list is a document (the form has an
829
+ * editor for it), and a date cell shows a formatted string over an
830
+ * epoch, so typing into it would write the string. Those stay with
831
+ * the form, which is one click away on the same row.
832
+ *
833
+ * The schema decides the rest: only a column flagged `writable` is
834
+ * offered, and never the pkey — renaming what a record is KEYED by
835
+ * is not a field edit.
836
+ */
837
+ const INLINE_EDITABLE = [
838
+ "string", "integer", "real",
839
+ "email", "url", "tel", "id", "hex", "currency", "percent", "uuid"
840
+ ];
841
+
842
+ function inline_editor_for(colDef, col, field_desc)
843
+ {
844
+ if(!field_desc.is_writable) {
845
+ return;
846
+ }
847
+ if(col.id === (desc.pkey || "id")) {
848
+ return;
849
+ }
850
+
851
+ switch(field_desc.type) {
852
+ case "boolean":
853
+ colDef.editor = "tickCross";
854
+ break;
855
+ case "enum":
856
+ colDef.editor = "list";
857
+ colDef.editorParams = {values: field_desc.enum_list || []};
858
+ break;
859
+ case "integer":
860
+ case "real":
861
+ colDef.editor = "number";
862
+ break;
863
+ default:
864
+ if(!INLINE_EDITABLE.includes(field_desc.type)) {
865
+ return;
866
+ }
867
+ colDef.editor = "input";
868
+ break;
869
+ }
870
+
871
+ /* A FUNCTION, not a flag: edition mode is toggled on a table that
872
+ * is already built (ac_edition_mode only shows and hides columns),
873
+ * so the answer has to be asked for at the moment of the click. */
874
+ colDef.editable = function() {
875
+ return gobj_read_bool_attr(gobj, "editable") &&
876
+ !gobj_read_bool_attr(gobj, "readonly");
877
+ };
878
+ }
879
+
822
880
  const FILTERABLE = [
823
881
  "string", "integer", "real", "rowid", "uuid", "qualified",
824
882
  "email", "url", "tel", "id", "hex", "currency", "percent"
@@ -974,6 +1032,9 @@ function create_tabulator(gobj)
974
1032
  if(with_header_filters) {
975
1033
  apply_header_filter(colDef, field_desc);
976
1034
  }
1035
+ if(with_inline_edit) {
1036
+ inline_editor_for(colDef, col, field_desc);
1037
+ }
977
1038
  if(vertAlign) {
978
1039
  colDef.vertAlign = vertAlign;
979
1040
  }
@@ -1139,6 +1200,33 @@ function create_tabulator(gobj)
1139
1200
  * box; a filter per column made it a claim you read on every keystroke. */
1140
1201
  tabulator.on("dataProcessed", update_rowcount);
1141
1202
  tabulator.on("dataChanged", update_rowcount);
1203
+ /* A cell edited in place. Announced as ONE FIELD of one record — not
1204
+ * as the row — and that is the whole safety of it: the host writes it
1205
+ * with a partial update and no `autolink`, so nothing but that field
1206
+ * moves. `autolink` wipes a node's links and rebuilds them from the
1207
+ * fkeys the record carries, so sending a row that the table shaped for
1208
+ * DISPLAY would unlink the node and answer success. */
1209
+ tabulator.on("cellEdited", function(cell) {
1210
+ let pkey = (gobj_read_attr(gobj, "desc") || {}).pkey || "id";
1211
+ let data = cell.getRow().getData();
1212
+ let id = data ? data[pkey] : undefined;
1213
+ if(id === undefined || id === null) {
1214
+ log_error(`${gobj_short_name(gobj)}: edited a row with no '${pkey}'`);
1215
+ cell.restoreOldValue();
1216
+ return;
1217
+ }
1218
+ gobj_send_event(
1219
+ gobj,
1220
+ "EV_CELL_EDITED",
1221
+ {
1222
+ id: id,
1223
+ field: cell.getField(),
1224
+ value: cell.getValue()
1225
+ },
1226
+ gobj
1227
+ );
1228
+ });
1229
+
1142
1230
  tabulator.on("dataFiltered", function(filters, rows) {
1143
1231
  update_rowcount(Array.isArray(rows)? rows.length : undefined);
1144
1232
  });
@@ -2845,6 +2933,28 @@ function ac_show_cell_json(gobj, event, kw, src)
2845
2933
  return 0;
2846
2934
  }
2847
2935
 
2936
+ /************************************************************
2937
+ * One cell of one record changed. Publish it UP as a field write;
2938
+ * the host owns the treedb command and its options.
2939
+ ************************************************************/
2940
+ function ac_cell_edited(gobj, event, kw, src)
2941
+ {
2942
+ if(refuse_if_readonly(gobj, event)) {
2943
+ return -1; /* Error already logged */
2944
+ }
2945
+ gobj_publish_event(
2946
+ gobj,
2947
+ "EV_UPDATE_FIELD",
2948
+ {
2949
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
2950
+ id: kw.id,
2951
+ field: kw.field,
2952
+ value: kw.value
2953
+ }
2954
+ );
2955
+ return 0;
2956
+ }
2957
+
2848
2958
  /************************************************************
2849
2959
  * Show the schema (desc) of this topic
2850
2960
  ************************************************************/
@@ -3076,6 +3186,7 @@ function create_gclass(gclass_name)
3076
3186
  ["EV_CHANGE_LOCALE", ac_change_locale, null],
3077
3187
  ["EV_REFRESH", ac_refresh, null],
3078
3188
  ["EV_SHOW_SCHEMA", ac_show_schema, null],
3189
+ ["EV_CELL_EDITED", ac_cell_edited, null],
3079
3190
  ["EV_SEARCH", ac_search, null],
3080
3191
  ["EV_OPEN_COLUMNS", ac_open_columns, null],
3081
3192
  ["EV_TOGGLE_COLUMN", ac_toggle_column, null],
@@ -3108,6 +3219,8 @@ function create_gclass(gclass_name)
3108
3219
  ["EV_CHANGE_LOCALE", 0],
3109
3220
  ["EV_REFRESH", 0],
3110
3221
  ["EV_SHOW_SCHEMA", 0],
3222
+ ["EV_CELL_EDITED", 0],
3223
+ ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
3111
3224
  ["EV_SEARCH", 0],
3112
3225
  ["EV_OPEN_COLUMNS", 0],
3113
3226
  ["EV_TOGGLE_COLUMN", 0],
@@ -1740,6 +1740,18 @@ function ac_mt_command_answer(gobj, event, kw, src)
1740
1740
  show_load_error(gobj, t(comment));
1741
1741
  } else {
1742
1742
  yui_shell_show_error(yui_shell_of(gobj), comment, {t: t});
1743
+ /* A refused write leaves the TABLE showing what the operator
1744
+ * typed and the store holding what it had. That is tolerable
1745
+ * for a form (it stays open on the values it failed with) and
1746
+ * is not for a cell edited in place, which looks saved. Put the
1747
+ * topic back to what the treedb actually has. */
1748
+ if(command === "update-node" || command === "create-node") {
1749
+ let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
1750
+ if(failed_topic) {
1751
+ treedb_nodes(gobj, gobj_read_str_attr(gobj, "treedb_name"),
1752
+ failed_topic, {list_dict: true});
1753
+ }
1754
+ }
1743
1755
  }
1744
1756
  return 0;
1745
1757
  }
@@ -2126,6 +2138,49 @@ function ac_create_record(gobj, event, kw, src)
2126
2138
  );
2127
2139
  }
2128
2140
 
2141
+ /********************************************
2142
+ * Event from formtable: ONE field of one record, edited in place.
2143
+ *
2144
+ * Written as a PARTIAL update and, deliberately, with NO `autolink`.
2145
+ * `treedb_update_node()` merges (`json_object_update`), so the fields
2146
+ * it does not carry are left alone; and `autolink` is what wipes a
2147
+ * node's links to rebuild them from the fkeys the record carries — on
2148
+ * a partial record that reads as "no parents" and DETACHES the node,
2149
+ * answering success. A cell edit changes a scalar and must not be able
2150
+ * to touch a link at all.
2151
+ *
2152
+ * This is why it is its own event and not EV_UPDATE_RECORD: that one
2153
+ * DOES send autolink, and may, because the form hands it the whole
2154
+ * record with its fkeys in it.
2155
+ ********************************************/
2156
+ function ac_update_field(gobj, event, kw, src)
2157
+ {
2158
+ if(refuse_if_readonly(gobj, event)) {
2159
+ return -1; /* Error already logged */
2160
+ }
2161
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
2162
+ let descs = gobj_read_attr(gobj, "descs") || {};
2163
+ let desc = descs[topic_name];
2164
+ let pkey = (desc && desc.pkey) || "id";
2165
+
2166
+ if(kw.id === undefined || kw.id === null || !kw.field) {
2167
+ log_error(`${gobj_short_name(gobj)}: a field write with no id or no field`);
2168
+ return -1;
2169
+ }
2170
+
2171
+ let record = {};
2172
+ record[pkey] = kw.id;
2173
+ record[kw.field] = kw.value;
2174
+
2175
+ return treedb_update_node(
2176
+ gobj,
2177
+ gobj_read_str_attr(gobj, "treedb_name"),
2178
+ topic_name,
2179
+ record,
2180
+ {list_dict: true}
2181
+ );
2182
+ }
2183
+
2129
2184
  /********************************************
2130
2185
  * Event from formtable
2131
2186
  * kw: {
@@ -2292,6 +2347,7 @@ function create_gclass(gclass_name)
2292
2347
  ["EV_TREEDB_NODE_DELETED", ac_treedb_node_deleted, null],
2293
2348
  ["EV_CREATE_RECORD", ac_create_record, null],
2294
2349
  ["EV_UPDATE_RECORD", ac_update_record, null],
2350
+ ["EV_UPDATE_FIELD", ac_update_field, null],
2295
2351
  ["EV_DELETE_RECORD", ac_delete_record, null],
2296
2352
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2297
2353
  ["EV_OPEN_JSON", ac_open_json, null],
@@ -2318,6 +2374,7 @@ function create_gclass(gclass_name)
2318
2374
  ["EV_TREEDB_NODE_DELETED", event_flag_t.EVF_PUBLIC_EVENT],
2319
2375
  ["EV_CREATE_RECORD", 0],
2320
2376
  ["EV_UPDATE_RECORD", 0],
2377
+ ["EV_UPDATE_FIELD", 0],
2321
2378
  ["EV_DELETE_RECORD", 0],
2322
2379
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
2323
2380
  event_flag_t.EVF_NO_WARN_SUBS],