@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.
- package/README.md +31 -1
- package/dist/gobj-ui.cjs.js +702 -1024
- package/dist/gobj-ui.es.js +703 -1025
- package/package.json +1 -1
- package/src/c_yui_form.js +322 -51
- package/src/c_yui_map.js +7 -4
- package/src/c_yui_nav.js +90 -1
- package/src/c_yui_shell.css +68 -2
- package/src/c_yui_shell.js +103 -18
- package/src/c_yui_treedb_topic_with_form.js +216 -1171
- package/src/nav_cards_helpers.js +71 -0
- package/src/nav_cards_helpers.test.js +122 -0
- package/src/shell_section_index.js +106 -0
- package/src/shell_section_index.test.js +151 -0
package/src/c_yui_form.js
CHANGED
|
@@ -45,12 +45,17 @@ import {
|
|
|
45
45
|
gobj_read_str_attr,
|
|
46
46
|
gobj_publish_event,
|
|
47
47
|
refresh_language,
|
|
48
|
+
treedb_decoder_fkey,
|
|
49
|
+
gclass_find_by_name,
|
|
48
50
|
} from "@yuneta/gobj-js";
|
|
49
51
|
|
|
50
52
|
import {t} from "i18next";
|
|
51
53
|
import "tom-select/dist/css/tom-select.css"; // Import Tom-Select CSS
|
|
52
54
|
import TomSelect from "tom-select"; // Import Tom-Select JS
|
|
53
55
|
|
|
56
|
+
import { JSONEditor } from 'vanilla-jsoneditor';
|
|
57
|
+
import "vanilla-jsoneditor/themes/jse-theme-dark.css";
|
|
58
|
+
|
|
54
59
|
import "tabulator-tables/dist/css/tabulator.min.css"; // Import Tabulator CSS
|
|
55
60
|
import "tabulator-tables/dist/css/tabulator_bulma.css";
|
|
56
61
|
import { TabulatorFull as Tabulator } from "tabulator-tables"; // Import Full Tabulator JS
|
|
@@ -74,6 +79,10 @@ SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will
|
|
|
74
79
|
SDATA(data_type_t.DTP_POINTER, "template", 0, "<div>You must supply a template for form</div>", "Template for form, object -> TEMPLATE, array -> DESC"),
|
|
75
80
|
SDATA(data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
|
|
76
81
|
SDATA(data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
|
|
82
|
+
SDATA(data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
|
|
83
|
+
SDATA(data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
|
|
84
|
+
SDATA(data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
|
|
85
|
+
SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
|
|
77
86
|
SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
|
|
78
87
|
SDATA(data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable with 'click' (BAD with editable=true)"),
|
|
79
88
|
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
|
|
@@ -284,31 +293,31 @@ function build_ui(gobj)
|
|
|
284
293
|
*
|
|
285
294
|
*----------------------------------------------*/
|
|
286
295
|
let $container = createElement2(
|
|
287
|
-
['div', {class: 'C_YUI_FORM is-flex is-flex-direction-
|
|
296
|
+
['div', {class: 'C_YUI_FORM is-flex is-flex-direction-column overscroll-contain', style:'height:100%; width:100%; box-sizing:border-box;'}, [
|
|
288
297
|
|
|
289
298
|
/*----------------------------*
|
|
290
299
|
* Form
|
|
291
300
|
*----------------------------*/
|
|
292
301
|
['div', {
|
|
293
302
|
class: 'yui-form is-flex-grow-1 p-2',
|
|
294
|
-
style: 'overflow-y:auto;
|
|
303
|
+
style: 'overflow-y:auto; min-height:0;'
|
|
295
304
|
}, build_form(gobj)],
|
|
296
305
|
|
|
297
306
|
/*----------------------------*
|
|
298
|
-
*
|
|
307
|
+
* Bottom toolbar
|
|
299
308
|
*----------------------------*/
|
|
300
|
-
['div', {class: 'yui-toolbar-form
|
|
309
|
+
['div', {class: 'yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center', style: 'width:100%; flex-wrap:wrap; row-gap:6px;'}, [
|
|
301
310
|
|
|
302
311
|
/*----------------------------*
|
|
303
|
-
*
|
|
312
|
+
* Left buttons
|
|
304
313
|
*----------------------------*/
|
|
305
|
-
['div', {class: 'p-1 is-flex is-flex-direction-
|
|
314
|
+
['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'}, [
|
|
306
315
|
/*----------------------------*
|
|
307
316
|
* Save
|
|
308
317
|
*----------------------------*/
|
|
309
|
-
['button', {class: 'button p-1
|
|
318
|
+
['button', {class: 'button p-1 button-save', title: 'save', 'aria-label': 'save', disabled: true}, [
|
|
310
319
|
['span', {class: 'icon m-0'}, '<i class="yi-floppy-disk"></i>'],
|
|
311
|
-
['span', {class: 'is-hidden-mobile', i18n: 'save'}, 'save']
|
|
320
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'save'}, 'save']
|
|
312
321
|
], {
|
|
313
322
|
click: function(evt) {
|
|
314
323
|
evt.stopPropagation();
|
|
@@ -319,9 +328,9 @@ function build_ui(gobj)
|
|
|
319
328
|
/*----------------------------*
|
|
320
329
|
* Undo
|
|
321
330
|
*----------------------------*/
|
|
322
|
-
['button', {class: 'button p-1
|
|
331
|
+
['button', {class: 'button p-1 button-undo', title: 'undo', 'aria-label': 'undo', disabled: true}, [
|
|
323
332
|
['span', {class: 'icon m-0'}, '<i class="yi-arrow-rotate-left"></i>'],
|
|
324
|
-
['span', {class: 'is-hidden-mobile', i18n: 'undo'}, 'undo']
|
|
333
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'undo'}, 'undo']
|
|
325
334
|
], {
|
|
326
335
|
click: function(evt) {
|
|
327
336
|
evt.stopPropagation();
|
|
@@ -332,9 +341,9 @@ function build_ui(gobj)
|
|
|
332
341
|
/*----------------------------*
|
|
333
342
|
* Clear
|
|
334
343
|
*----------------------------*/
|
|
335
|
-
['button', {class: 'button p-1
|
|
344
|
+
['button', {class: 'button p-1', title: 'clear', 'aria-label': 'clear'}, [
|
|
336
345
|
['span', {class: 'icon m-0'}, '<i class="yi-broom-wide"></i>'],
|
|
337
|
-
['span', {class: 'is-hidden-mobile', i18n: 'clear'}, 'clear']
|
|
346
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'clear'}, 'clear']
|
|
338
347
|
], {
|
|
339
348
|
click: function(evt) {
|
|
340
349
|
evt.stopPropagation();
|
|
@@ -344,15 +353,15 @@ function build_ui(gobj)
|
|
|
344
353
|
]],
|
|
345
354
|
|
|
346
355
|
/*----------------------------*
|
|
347
|
-
*
|
|
356
|
+
* Right buttons
|
|
348
357
|
*----------------------------*/
|
|
349
|
-
['div', {class: 'p-1 is-flex is-flex-direction-
|
|
358
|
+
['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'}, [
|
|
350
359
|
/*----------------------------*
|
|
351
360
|
* Copy
|
|
352
361
|
*----------------------------*/
|
|
353
|
-
['button', {class: 'button p-1
|
|
362
|
+
['button', {class: 'button p-1', title: 'copy', 'aria-label': 'copy'}, [
|
|
354
363
|
['span', {class: 'icon m-0'}, '<i class="yi-copy"></i>'],
|
|
355
|
-
['span', {class: 'is-hidden-mobile', i18n: 'copy'}, 'copy']
|
|
364
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'copy'}, 'copy']
|
|
356
365
|
], {click: function(evt) {
|
|
357
366
|
evt.stopPropagation();
|
|
358
367
|
gobj_send_event(gobj, "EV_COPY_RECORD", {}, gobj);
|
|
@@ -362,9 +371,9 @@ function build_ui(gobj)
|
|
|
362
371
|
/*----------------------------*
|
|
363
372
|
* Paste
|
|
364
373
|
*----------------------------*/
|
|
365
|
-
['button', {class: 'button p-1
|
|
374
|
+
['button', {class: 'button p-1', title: 'paste', 'aria-label': 'paste'}, [
|
|
366
375
|
['span', {class: 'icon m-0'}, '<i class="yi-paste"></i>'],
|
|
367
|
-
['span', {class: 'is-hidden-mobile', i18n: 'paste'}, 'paste']
|
|
376
|
+
['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'paste'}, 'paste']
|
|
368
377
|
], {click: async function(evt) {
|
|
369
378
|
evt.stopPropagation();
|
|
370
379
|
await readClipboard(gobj);
|
|
@@ -376,6 +385,7 @@ function build_ui(gobj)
|
|
|
376
385
|
);
|
|
377
386
|
|
|
378
387
|
gobj_write_attr(gobj, "$container", $container);
|
|
388
|
+
apply_form_mode(gobj, $container.querySelector('form'));
|
|
379
389
|
refresh_language($container, t);
|
|
380
390
|
|
|
381
391
|
let $parent = gobj_read_attr(gobj, "$parent");
|
|
@@ -446,7 +456,25 @@ function build_html_form(gobj, $form, prefix, template)
|
|
|
446
456
|
* items not filled
|
|
447
457
|
*/
|
|
448
458
|
html_field_conf.name = field_desc.name;
|
|
449
|
-
|
|
459
|
+
/*
|
|
460
|
+
* Label i18n: mirror the table-header cascade (col_label) so a
|
|
461
|
+
* column translates the same in the form and the table. The
|
|
462
|
+
* visible text resolves '<topic>.<col>' -> '<col>' -> header at
|
|
463
|
+
* build. Only set `label_i18n` (the retranslation key) when a
|
|
464
|
+
* translation actually exists, so refresh_language never
|
|
465
|
+
* clobbers an untranslated label with the raw col id — it keeps
|
|
466
|
+
* the header, exactly like col_label's `defaultValue`.
|
|
467
|
+
*/
|
|
468
|
+
let topic_name = gobj_read_str_attr(gobj, "topic_name");
|
|
469
|
+
let label_keys = topic_name
|
|
470
|
+
? [topic_name + "." + field_desc.name, field_desc.name]
|
|
471
|
+
: [field_desc.name];
|
|
472
|
+
const NO_I18N = "\x00";
|
|
473
|
+
html_field_conf.label = t(label_keys, {defaultValue: field_desc.header});
|
|
474
|
+
html_field_conf.label_i18n =
|
|
475
|
+
(t(field_desc.name, {defaultValue: NO_I18N}) !== NO_I18N)
|
|
476
|
+
? field_desc.name
|
|
477
|
+
: null;
|
|
450
478
|
html_field_conf.placeholder = field_desc.placeholder?t(field_desc.placeholder):'';
|
|
451
479
|
|
|
452
480
|
let placeholders = gobj_read_attr(gobj, "placeholders");
|
|
@@ -502,12 +530,13 @@ function build_form_field_conf(gobj, field_desc)
|
|
|
502
530
|
switch(field_desc.type) {
|
|
503
531
|
case "object":
|
|
504
532
|
case "dict":
|
|
505
|
-
|
|
533
|
+
/* free-form dict (no subschema mechanism) edits as raw
|
|
534
|
+
* JSON; structured dicts use the "template" flag */
|
|
535
|
+
field_conf.tag = "jsoneditor";
|
|
506
536
|
break;
|
|
507
537
|
|
|
508
538
|
case "template":
|
|
509
539
|
field_conf.tag = "fieldset";
|
|
510
|
-
// TODO si es para editar, tiene que ser field_conf.tag = "jsoneditor";
|
|
511
540
|
field_conf.options = field_desc.enum_list;
|
|
512
541
|
break;
|
|
513
542
|
|
|
@@ -522,7 +551,9 @@ function build_form_field_conf(gobj, field_desc)
|
|
|
522
551
|
|
|
523
552
|
case "array":
|
|
524
553
|
case "list":
|
|
525
|
-
|
|
554
|
+
/* free-form list (no subschema) edits as raw JSON;
|
|
555
|
+
* structured lists use the "table" flag */
|
|
556
|
+
field_conf.tag = "jsoneditor";
|
|
526
557
|
break;
|
|
527
558
|
|
|
528
559
|
case "enum":
|
|
@@ -546,7 +577,8 @@ function build_form_field_conf(gobj, field_desc)
|
|
|
546
577
|
break;
|
|
547
578
|
|
|
548
579
|
case "fkey":
|
|
549
|
-
|
|
580
|
+
field_conf.tag = "select2";
|
|
581
|
+
field_conf.options = get_fkey_select_options(gobj, field_desc);
|
|
550
582
|
break;
|
|
551
583
|
|
|
552
584
|
case "image":
|
|
@@ -660,6 +692,149 @@ function build_form_field_conf(gobj, field_desc)
|
|
|
660
692
|
return field_conf;
|
|
661
693
|
}
|
|
662
694
|
|
|
695
|
+
/************************************************************
|
|
696
|
+
* Options for a fkey field: the ids of the parent topic's
|
|
697
|
+
* rows, taken from the `fkey_options` attr
|
|
698
|
+
* ({topic_name: [ids or {id} records]}, supplied by the host —
|
|
699
|
+
* the form never queries the backend or its parent itself).
|
|
700
|
+
* Read at build time.
|
|
701
|
+
************************************************************/
|
|
702
|
+
function get_fkey_select_options(gobj, field_desc)
|
|
703
|
+
{
|
|
704
|
+
let fkey_desc = field_desc.fkey;
|
|
705
|
+
if(!is_object(fkey_desc) || Object.keys(fkey_desc).length === 0) {
|
|
706
|
+
log_error(sprintf("%s: fkey field '%s' without fkey mapping",
|
|
707
|
+
gobj_short_name(gobj), field_desc.name
|
|
708
|
+
));
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// HACK we work only with one fkey (same as the treedb stack)
|
|
713
|
+
let topic_name = Object.keys(fkey_desc)[0];
|
|
714
|
+
|
|
715
|
+
let fkey_options = gobj_read_attr(gobj, "fkey_options");
|
|
716
|
+
let list = is_object(fkey_options)? fkey_options[topic_name] : null;
|
|
717
|
+
if(!is_array(list)) {
|
|
718
|
+
log_warning(sprintf("%s: no fkey_options for topic '%s' (field '%s')",
|
|
719
|
+
gobj_short_name(gobj), topic_name, field_desc.name
|
|
720
|
+
));
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let options = [];
|
|
725
|
+
for(let item of list) {
|
|
726
|
+
let id = is_object(item)? item.id : item;
|
|
727
|
+
if(is_string(id) && !empty_string(id)) {
|
|
728
|
+
options.push(id);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return options;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/************************************************************
|
|
735
|
+
* Encode the select2 value (array of ids) into fkey ref(s)
|
|
736
|
+
* "topic_name^id^hook_name", shaped by the column real type:
|
|
737
|
+
* string -> single ref (or null if no selection)
|
|
738
|
+
* object/dict -> {ref: true, ...}
|
|
739
|
+
* array/list -> [ref, ...]
|
|
740
|
+
************************************************************/
|
|
741
|
+
function build_fkey_ref(gobj, field_desc, value)
|
|
742
|
+
{
|
|
743
|
+
let fkey_desc = field_desc.fkey;
|
|
744
|
+
if(!is_object(fkey_desc) || Object.keys(fkey_desc).length === 0) {
|
|
745
|
+
log_error(sprintf("%s: fkey field '%s' without fkey mapping",
|
|
746
|
+
gobj_short_name(gobj), field_desc.name
|
|
747
|
+
));
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// HACK we work only with one fkey (same as the treedb stack)
|
|
752
|
+
let topic_name = Object.keys(fkey_desc)[0];
|
|
753
|
+
let hook_name = fkey_desc[topic_name];
|
|
754
|
+
|
|
755
|
+
let refs;
|
|
756
|
+
switch(field_desc.real_type) {
|
|
757
|
+
case "string":
|
|
758
|
+
refs = null;
|
|
759
|
+
break;
|
|
760
|
+
case "object":
|
|
761
|
+
case "dict":
|
|
762
|
+
refs = {};
|
|
763
|
+
break;
|
|
764
|
+
case "array":
|
|
765
|
+
case "list":
|
|
766
|
+
refs = [];
|
|
767
|
+
break;
|
|
768
|
+
default:
|
|
769
|
+
log_error(sprintf("%s: fkey field '%s' with bad real_type '%s'",
|
|
770
|
+
gobj_short_name(gobj), field_desc.name, field_desc.real_type
|
|
771
|
+
));
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if(!is_array(value)) {
|
|
776
|
+
value = value? [value] : [];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
for(let v of value) {
|
|
780
|
+
let id = is_object(v)? v.id : v;
|
|
781
|
+
if(!is_string(id) || empty_string(id)) {
|
|
782
|
+
log_error(sprintf("%s: fkey field '%s' with bad value: %s",
|
|
783
|
+
gobj_short_name(gobj), field_desc.name, JSON.stringify(v)
|
|
784
|
+
));
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
let ref = topic_name + "^" + id + "^" + hook_name;
|
|
788
|
+
switch(field_desc.real_type) {
|
|
789
|
+
case "string":
|
|
790
|
+
return ref; // single-valued: first id wins
|
|
791
|
+
case "object":
|
|
792
|
+
case "dict":
|
|
793
|
+
refs[ref] = true;
|
|
794
|
+
break;
|
|
795
|
+
case "array":
|
|
796
|
+
case "list":
|
|
797
|
+
refs.push(ref);
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return refs;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/************************************************************
|
|
806
|
+
* Apply the form mode to the pkey field:
|
|
807
|
+
* "update" -> pkey readonly, not required
|
|
808
|
+
* "create" -> pkey writable and required (readonly if rowid)
|
|
809
|
+
* Empty form_mode keeps the template-declared behaviour
|
|
810
|
+
* (backward compatible).
|
|
811
|
+
************************************************************/
|
|
812
|
+
function apply_form_mode(gobj, $form)
|
|
813
|
+
{
|
|
814
|
+
let mode = gobj_read_str_attr(gobj, "form_mode");
|
|
815
|
+
if(empty_string(mode)) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let pkey = gobj_read_str_attr(gobj, "pkey");
|
|
820
|
+
let $input = $form.querySelector(`[name="${pkey}"]`);
|
|
821
|
+
if(!$input) {
|
|
822
|
+
log_error(sprintf("%s: form_mode '%s' but form has no pkey field '%s'",
|
|
823
|
+
gobj_short_name(gobj), mode, pkey
|
|
824
|
+
));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let is_rowid = $input.field_desc && $input.field_desc.type === "rowid";
|
|
829
|
+
if(mode === "create" && !is_rowid) {
|
|
830
|
+
$input.removeAttribute("readonly");
|
|
831
|
+
$input.setAttribute("required", "");
|
|
832
|
+
} else {
|
|
833
|
+
$input.setAttribute("readonly", "readonly");
|
|
834
|
+
$input.removeAttribute("required");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
663
838
|
/******************************************************************
|
|
664
839
|
* With html field conf builds an html element,
|
|
665
840
|
* ready to add to the form
|
|
@@ -700,6 +875,7 @@ function create_form_field(
|
|
|
700
875
|
inputType,
|
|
701
876
|
name,
|
|
702
877
|
label,
|
|
878
|
+
label_i18n,
|
|
703
879
|
placeholder,
|
|
704
880
|
options,
|
|
705
881
|
extras, // extra html input/textarea attributes
|
|
@@ -750,7 +926,7 @@ function create_form_field(
|
|
|
750
926
|
$element = createElement2(
|
|
751
927
|
['div', {class: 'xfield field is-horizontal'}, [
|
|
752
928
|
['div', {class: 'field-label is-normal' }, [
|
|
753
|
-
[
|
|
929
|
+
["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
|
|
754
930
|
]],
|
|
755
931
|
|
|
756
932
|
['div', {class: 'field-body'}, [
|
|
@@ -765,7 +941,7 @@ function create_form_field(
|
|
|
765
941
|
$element = createElement2(
|
|
766
942
|
['div', {class: 'xfield field is-horizontal'}, [
|
|
767
943
|
['div', {class: 'field-label is-normal' }, [
|
|
768
|
-
[
|
|
944
|
+
["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
|
|
769
945
|
]],
|
|
770
946
|
|
|
771
947
|
['div', {class: 'field-body'}, [
|
|
@@ -788,7 +964,7 @@ function create_form_field(
|
|
|
788
964
|
$element = createElement2(
|
|
789
965
|
['div', {class: 'xfield field is-horizontal'}, [
|
|
790
966
|
['div', {class: 'field-label is-normal' }, [
|
|
791
|
-
[
|
|
967
|
+
["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
|
|
792
968
|
]],
|
|
793
969
|
|
|
794
970
|
['div', {class: 'field-body'}, [
|
|
@@ -804,7 +980,7 @@ function create_form_field(
|
|
|
804
980
|
$element = createElement2(
|
|
805
981
|
['div', {class: 'xfield field is-horizontal'}, [
|
|
806
982
|
['div', {class: 'field-label is-normal' }, [
|
|
807
|
-
[
|
|
983
|
+
["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
|
|
808
984
|
]],
|
|
809
985
|
|
|
810
986
|
['div', {class: 'field-body'}, [
|
|
@@ -823,7 +999,7 @@ function create_form_field(
|
|
|
823
999
|
$element = createElement2(
|
|
824
1000
|
['div', {class: 'field is-horizontal tabulator-buttons'}, [
|
|
825
1001
|
['div', {class: 'field-label is-normal' }, [
|
|
826
|
-
['label', {class:'label', i18n:label}, label]
|
|
1002
|
+
['label', {class:'label', i18n:(label_i18n||label)}, label]
|
|
827
1003
|
]],
|
|
828
1004
|
|
|
829
1005
|
['div', {class: 'field-body'}, [
|
|
@@ -866,7 +1042,7 @@ function create_form_field(
|
|
|
866
1042
|
$element = createElement2(
|
|
867
1043
|
['div', {class: 'xfield field is-horizontal'}, [
|
|
868
1044
|
['div', {class: 'field-label is-normal' }, [
|
|
869
|
-
[
|
|
1045
|
+
["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
|
|
870
1046
|
]],
|
|
871
1047
|
|
|
872
1048
|
['div', {class: 'field-body'}, [
|
|
@@ -895,7 +1071,7 @@ function create_form_field(
|
|
|
895
1071
|
case 'input':
|
|
896
1072
|
{
|
|
897
1073
|
let attrs = {
|
|
898
|
-
class: `input ${name===
|
|
1074
|
+
class: `input ${name===gobj_read_str_attr(gobj, "pkey")?'with-focus':''}`,
|
|
899
1075
|
name: name,
|
|
900
1076
|
type: inputType,
|
|
901
1077
|
placeholder: placeholder
|
|
@@ -968,11 +1144,19 @@ function create_form_field(
|
|
|
968
1144
|
},
|
|
969
1145
|
options.map(option =>
|
|
970
1146
|
['option', {value:option, i18n:option}, option]
|
|
971
|
-
)
|
|
1147
|
+
),
|
|
1148
|
+
{
|
|
1149
|
+
'change': function (evt) {
|
|
1150
|
+
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
972
1153
|
]
|
|
973
1154
|
];
|
|
974
|
-
$
|
|
975
|
-
$control.appendChild($
|
|
1155
|
+
let $wrapper = createElement2(extend);
|
|
1156
|
+
$control.appendChild($wrapper);
|
|
1157
|
+
/* the data-input marker must land on the SELECT, not on the
|
|
1158
|
+
* bulma .select wrapper — get/set read/write $input.value */
|
|
1159
|
+
$extend = $wrapper.querySelector('select');
|
|
976
1160
|
break;
|
|
977
1161
|
}
|
|
978
1162
|
|
|
@@ -996,6 +1180,9 @@ function create_form_field(
|
|
|
996
1180
|
$extend.tom_select = new TomSelect($extend, {
|
|
997
1181
|
allowEmptyOption: true,
|
|
998
1182
|
placeholder: placeholder,
|
|
1183
|
+
/* single-valued fkey (real_type string): one selection only */
|
|
1184
|
+
maxItems: (field_desc.type === "fkey" &&
|
|
1185
|
+
field_desc.real_type === "string")? 1 : null,
|
|
999
1186
|
plugins: {
|
|
1000
1187
|
'clear_button': {
|
|
1001
1188
|
'title': t('Remove all selected options'),
|
|
@@ -1014,10 +1201,17 @@ function create_form_field(
|
|
|
1014
1201
|
case 'checkbox':
|
|
1015
1202
|
{
|
|
1016
1203
|
let extend = ['label', {class: 'checkbox'},
|
|
1017
|
-
['input', {type: 'checkbox', name:name}
|
|
1204
|
+
['input', {type: 'checkbox', name:name}, '', {
|
|
1205
|
+
'change': function (evt) {
|
|
1206
|
+
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
1207
|
+
}
|
|
1208
|
+
}]
|
|
1018
1209
|
];
|
|
1019
|
-
$
|
|
1020
|
-
$control.appendChild($
|
|
1210
|
+
let $wrapper = createElement2(extend);
|
|
1211
|
+
$control.appendChild($wrapper);
|
|
1212
|
+
/* the data-input marker must land on the checkbox INPUT,
|
|
1213
|
+
* not on the label — get/set read/write $input.checked */
|
|
1214
|
+
$extend = $wrapper.querySelector('input');
|
|
1021
1215
|
break;
|
|
1022
1216
|
}
|
|
1023
1217
|
|
|
@@ -1043,6 +1237,7 @@ function create_form_field(
|
|
|
1043
1237
|
{
|
|
1044
1238
|
let attrs = {
|
|
1045
1239
|
class: 'jsoneditor jse-theme-dark',
|
|
1240
|
+
name: name,
|
|
1046
1241
|
style: ''
|
|
1047
1242
|
};
|
|
1048
1243
|
Object.assign(attrs, extras);
|
|
@@ -1055,6 +1250,22 @@ function create_form_field(
|
|
|
1055
1250
|
let extend = ['div', attrs];
|
|
1056
1251
|
$extend = createElement2(extend);
|
|
1057
1252
|
$control.appendChild($extend);
|
|
1253
|
+
$extend.jsoneditor = new JSONEditor({
|
|
1254
|
+
target: $extend,
|
|
1255
|
+
props: {
|
|
1256
|
+
readOnly: !!readonly,
|
|
1257
|
+
onChange: function() {
|
|
1258
|
+
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
1259
|
+
},
|
|
1260
|
+
timestampTag: function({field, value, path}) {
|
|
1261
|
+
return field === '__t__' || field === '__tm__' ||
|
|
1262
|
+
field === 'tm' || field === 't' || field === 'time' ||
|
|
1263
|
+
field === 'from_t' || field === 'to_t' ||
|
|
1264
|
+
field === 't_input' || field === 't_output' ||
|
|
1265
|
+
field === 'from_tm' || field === 'to_tm';
|
|
1266
|
+
},
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1058
1269
|
break;
|
|
1059
1270
|
}
|
|
1060
1271
|
|
|
@@ -1828,8 +2039,9 @@ function get_form_values(gobj, $form)
|
|
|
1828
2039
|
************************************************************/
|
|
1829
2040
|
function clear_data(gobj, $form)
|
|
1830
2041
|
{
|
|
2042
|
+
let pkey = gobj_read_str_attr(gobj, "pkey");
|
|
1831
2043
|
$form.querySelectorAll('input, select, textarea, .jsoneditor').forEach($input => {
|
|
1832
|
-
if($input.name !==
|
|
2044
|
+
if($input.name !== pkey) {
|
|
1833
2045
|
if($input.classList.contains("jsoneditor")) {
|
|
1834
2046
|
let jsoneditor = $input.jsoneditor;
|
|
1835
2047
|
if(jsoneditor) {
|
|
@@ -1923,7 +2135,7 @@ function set_form_values(gobj, template, $form, record)
|
|
|
1923
2135
|
|
|
1924
2136
|
clear_data(gobj, $form);
|
|
1925
2137
|
|
|
1926
|
-
if(!record || record.length === 0) {
|
|
2138
|
+
if(!record || Object.keys(record).length === 0) {
|
|
1927
2139
|
record = create_template_record(template);
|
|
1928
2140
|
}
|
|
1929
2141
|
|
|
@@ -1948,12 +2160,28 @@ function set_form_values(gobj, template, $form, record)
|
|
|
1948
2160
|
modified = true;
|
|
1949
2161
|
}
|
|
1950
2162
|
value = treedb_value_2_form_value(gobj, field_desc, value);
|
|
2163
|
+
if(value === undefined) {
|
|
2164
|
+
/* DOM 'value' setters stringify undefined as "undefined";
|
|
2165
|
+
* null is safe (LegacyNullToEmptyString). */
|
|
2166
|
+
value = null;
|
|
2167
|
+
}
|
|
1951
2168
|
|
|
1952
2169
|
switch(tag) {
|
|
1953
2170
|
case "input":
|
|
1954
2171
|
switch(input_type) {
|
|
1955
2172
|
case "datetime-local":
|
|
1956
|
-
|
|
2173
|
+
/* value is a Date (see treedb_value_2_form_value);
|
|
2174
|
+
* a datetime-local input wants local
|
|
2175
|
+
* "YYYY-MM-DDTHH:mm" (no seconds). */
|
|
2176
|
+
if(value instanceof Date && !isNaN(value.getTime())) {
|
|
2177
|
+
let pad = (n) => String(n).padStart(2, "0");
|
|
2178
|
+
$input.value =
|
|
2179
|
+
`${value.getFullYear()}-${pad(value.getMonth() + 1)}` +
|
|
2180
|
+
`-${pad(value.getDate())}T` +
|
|
2181
|
+
`${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
2182
|
+
} else {
|
|
2183
|
+
$input.value = "";
|
|
2184
|
+
}
|
|
1957
2185
|
break;
|
|
1958
2186
|
case "color":
|
|
1959
2187
|
$input.value = convertColor(value);
|
|
@@ -1988,7 +2216,7 @@ function set_form_values(gobj, template, $form, record)
|
|
|
1988
2216
|
case "jsoneditor":
|
|
1989
2217
|
let jsoneditor = $input.jsoneditor;
|
|
1990
2218
|
if(jsoneditor) {
|
|
1991
|
-
jsoneditor.set(value);
|
|
2219
|
+
jsoneditor.set(value ? value : {text: ''});
|
|
1992
2220
|
}
|
|
1993
2221
|
break;
|
|
1994
2222
|
|
|
@@ -2000,7 +2228,7 @@ function set_form_values(gobj, template, $form, record)
|
|
|
2000
2228
|
break;
|
|
2001
2229
|
|
|
2002
2230
|
case "table":
|
|
2003
|
-
load_tabulator_data(gobj, $input, value);
|
|
2231
|
+
load_tabulator_data(gobj, $input, value ? value : []);
|
|
2004
2232
|
break;
|
|
2005
2233
|
|
|
2006
2234
|
case "coordinates":
|
|
@@ -2080,7 +2308,35 @@ function treedb_value_2_form_value(gobj, field_desc, value)
|
|
|
2080
2308
|
let real_type = field_desc.real_type;
|
|
2081
2309
|
|
|
2082
2310
|
switch(type) {
|
|
2083
|
-
case "fkey":
|
|
2311
|
+
case "fkey": {
|
|
2312
|
+
/* Decode ref(s) ("topic^id^hook", bare "$id", dict keys or
|
|
2313
|
+
* {topic_name,id,hook_name}) into a plain array of ids for
|
|
2314
|
+
* the select2 widget. */
|
|
2315
|
+
let new_value = [];
|
|
2316
|
+
if(is_string(value) && !empty_string(value)) {
|
|
2317
|
+
let fkey = treedb_decoder_fkey(field_desc, value);
|
|
2318
|
+
if(fkey) {
|
|
2319
|
+
new_value.push(fkey.id);
|
|
2320
|
+
}
|
|
2321
|
+
} else if(is_array(value)) {
|
|
2322
|
+
for(let v of value) {
|
|
2323
|
+
let fkey = treedb_decoder_fkey(field_desc, v);
|
|
2324
|
+
if(fkey) {
|
|
2325
|
+
new_value.push(fkey.id);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
} else if(is_object(value)) {
|
|
2329
|
+
// dict-type fkeys store refs as keys {"topic^id^hook": true}
|
|
2330
|
+
for(let v of Object.keys(value)) {
|
|
2331
|
+
let fkey = treedb_decoder_fkey(field_desc, v);
|
|
2332
|
+
if(fkey) {
|
|
2333
|
+
new_value.push(fkey.id);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
value = new_value;
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2084
2340
|
case "hook":
|
|
2085
2341
|
case "enum":
|
|
2086
2342
|
case "uuid":
|
|
@@ -2091,8 +2347,9 @@ function treedb_value_2_form_value(gobj, field_desc, value)
|
|
|
2091
2347
|
break;
|
|
2092
2348
|
case "now":
|
|
2093
2349
|
case "time":
|
|
2094
|
-
|
|
2095
|
-
|
|
2350
|
+
/* Stored epoch is in SECONDS (Yuneta/timeranger convention),
|
|
2351
|
+
* matching get_form_values() which saves getTime()/1000. */
|
|
2352
|
+
value = new Date((parseInt(value) || 0) * 1000);
|
|
2096
2353
|
break;
|
|
2097
2354
|
case "date":
|
|
2098
2355
|
case "color":
|
|
@@ -2110,17 +2367,22 @@ function treedb_value_2_form_value(gobj, field_desc, value)
|
|
|
2110
2367
|
|
|
2111
2368
|
case "string":
|
|
2112
2369
|
case "integer":
|
|
2113
|
-
case "object":
|
|
2114
|
-
case "dict":
|
|
2115
|
-
case "array":
|
|
2116
|
-
case "list":
|
|
2117
2370
|
case "real":
|
|
2118
2371
|
case "boolean":
|
|
2119
2372
|
break;
|
|
2373
|
+
case "object":
|
|
2374
|
+
case "dict":
|
|
2375
|
+
/* jsoneditor Content shape */
|
|
2376
|
+
value = {json: is_object(value)? value : {}};
|
|
2377
|
+
break;
|
|
2120
2378
|
case "blob":
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2379
|
+
/* jsoneditor Content shape (a blob may hold any json) */
|
|
2380
|
+
value = {json: (is_object(value) || is_array(value))? value : {}};
|
|
2381
|
+
break;
|
|
2382
|
+
case "array":
|
|
2383
|
+
case "list":
|
|
2384
|
+
/* jsoneditor Content shape */
|
|
2385
|
+
value = {json: is_array(value)? value : []};
|
|
2124
2386
|
break;
|
|
2125
2387
|
case "number":
|
|
2126
2388
|
break;
|
|
@@ -2173,6 +2435,7 @@ function form_value_2_treedb_value(gobj, field_desc, value)
|
|
|
2173
2435
|
|
|
2174
2436
|
switch(type) {
|
|
2175
2437
|
case "fkey":
|
|
2438
|
+
value = build_fkey_ref(gobj, field_desc, value);
|
|
2176
2439
|
break;
|
|
2177
2440
|
case "hook":
|
|
2178
2441
|
value = null;
|
|
@@ -2334,6 +2597,7 @@ function ac_load_record(gobj, event, kw, src)
|
|
|
2334
2597
|
|
|
2335
2598
|
gobj_write_bool_attr(gobj, "initialized", false);
|
|
2336
2599
|
let modified = set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, kw);
|
|
2600
|
+
apply_form_mode(gobj, $form);
|
|
2337
2601
|
set_changed_stated(gobj, modified);
|
|
2338
2602
|
gobj_write_bool_attr(gobj, "initialized", true);
|
|
2339
2603
|
|
|
@@ -2619,6 +2883,13 @@ function create_gclass(gclass_name)
|
|
|
2619
2883
|
***************************************************************/
|
|
2620
2884
|
function register_c_yui_form()
|
|
2621
2885
|
{
|
|
2886
|
+
/* Idempotent: C_YUI_TREEDB_TOPIC_WITH_FORM auto-registers this
|
|
2887
|
+
* gclass for its hosted edit dialog, so an app that also registers
|
|
2888
|
+
* it explicitly (order-independent) must not trip
|
|
2889
|
+
* "GClass ALREADY created". */
|
|
2890
|
+
if(gclass_find_by_name(GCLASS_NAME, false)) {
|
|
2891
|
+
return 0;
|
|
2892
|
+
}
|
|
2622
2893
|
return create_gclass(GCLASS_NAME);
|
|
2623
2894
|
}
|
|
2624
2895
|
|