@yuneta/gobj-ui 7.23.168 → 7.23.169
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 +2 -2
- package/dist/gobj-ui.cjs.js +390 -92
- package/dist/gobj-ui.es.js +390 -92
- package/package.json +1 -1
- package/src/broadcast_attrs.test.js +115 -0
- package/src/c_g6_nodes_tree.js +208 -19
- package/src/c_yui_treedb_topic_with_form.js +201 -68
- package/src/yui_row_search.js +56 -27
- package/src/yui_row_search.test.js +51 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
json_deep_copy,
|
|
24
24
|
createElement2,
|
|
25
25
|
gobj_send_event,
|
|
26
|
+
gobj_post_event,
|
|
26
27
|
clean_name,
|
|
27
28
|
is_array,
|
|
28
29
|
is_string,
|
|
@@ -209,8 +210,8 @@ SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"
|
|
|
209
210
|
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
|
|
210
211
|
SDATA(data_type_t.DTP_BOOLEAN, "with_radio", 0, false, "Auxiliary first column to select one row"),
|
|
211
212
|
SDATA(data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Show the shared selection bar (\"N selected\" + a way out) above the table while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, and a host that has not defined \"{{n}} selected\" and \"clear selection\" would render the keys"),
|
|
212
|
-
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "
|
|
213
|
-
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "
|
|
213
|
+
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "Publish EV_SELECT_ROWS to the subscriber. OFF by default: the event is an OUTPUT event, so a host that turns it on must declare EV_SELECT_ROWS in its own FSM or it answers \"Event NOT DEFINED in state\" on every selected row"),
|
|
214
|
+
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "Publish EV_UNSELECT_ROWS to the subscriber. Same contract as broadcast_select_rows_event, and INDEPENDENT of it: a host that wants both sets both"),
|
|
214
215
|
|
|
215
216
|
/*---------------- Tabulator Defaults ----------------*/
|
|
216
217
|
SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
|
|
@@ -605,7 +606,9 @@ function build_ui(gobj)
|
|
|
605
606
|
* instead of being pushed away from the harmless ones. Do not
|
|
606
607
|
* reorder this group as if they were peer actions. */
|
|
607
608
|
let $edit_button = createElement2(
|
|
608
|
-
['button', {id: ``, class: 'button button-edit-record mr-1'
|
|
609
|
+
['button', {id: ``, class: 'button button-edit-record mr-1',
|
|
610
|
+
title: t('edit'), 'data-i18n-title': 'edit',
|
|
611
|
+
'aria-label': t('edit'), 'data-i18n-aria-label': 'edit'}, [
|
|
609
612
|
yui_toolbar_icon('yi-pen'),
|
|
610
613
|
['span',
|
|
611
614
|
{
|
|
@@ -631,7 +634,9 @@ function build_ui(gobj)
|
|
|
631
634
|
|
|
632
635
|
if(with_new_button) {
|
|
633
636
|
let $new_button = createElement2(
|
|
634
|
-
['button', {id: ``, class: 'button button-new-record mr-1',
|
|
637
|
+
['button', {id: ``, class: 'button button-new-record mr-1',
|
|
638
|
+
title: t('new'), 'data-i18n-title': 'new',
|
|
639
|
+
'aria-label': t('new'), 'data-i18n-aria-label': 'new', disabled: true}, [
|
|
635
640
|
yui_toolbar_icon('yi-plus'),
|
|
636
641
|
['span',
|
|
637
642
|
{
|
|
@@ -651,7 +656,9 @@ function build_ui(gobj)
|
|
|
651
656
|
|
|
652
657
|
if(with_delete_button) {
|
|
653
658
|
let $delete_button = createElement2(
|
|
654
|
-
['button', {id: ``, class: 'button button-delete-record mr-1',
|
|
659
|
+
['button', {id: ``, class: 'button button-delete-record mr-1',
|
|
660
|
+
title: t('delete'), 'data-i18n-title': 'delete',
|
|
661
|
+
'aria-label': t('delete'), 'data-i18n-aria-label': 'delete', disabled: true}, [
|
|
655
662
|
yui_toolbar_icon('yi-trash'),
|
|
656
663
|
['span',
|
|
657
664
|
{
|
|
@@ -672,7 +679,9 @@ function build_ui(gobj)
|
|
|
672
679
|
|
|
673
680
|
if(with_copy_button) {
|
|
674
681
|
let $copy_button = createElement2(
|
|
675
|
-
['button', {id: ``, class: 'button button-copy-record mr-1',
|
|
682
|
+
['button', {id: ``, class: 'button button-copy-record mr-1',
|
|
683
|
+
title: t('copy'), 'data-i18n-title': 'copy',
|
|
684
|
+
'aria-label': t('copy'), 'data-i18n-aria-label': 'copy', disabled: true}, [
|
|
676
685
|
yui_toolbar_icon('yi-copy'),
|
|
677
686
|
['span',
|
|
678
687
|
{
|
|
@@ -693,7 +702,9 @@ function build_ui(gobj)
|
|
|
693
702
|
|
|
694
703
|
if(with_paste_button) {
|
|
695
704
|
let $paste_button = createElement2(
|
|
696
|
-
['button', {id: ``, class: 'button button-paste-record mr-1',
|
|
705
|
+
['button', {id: ``, class: 'button button-paste-record mr-1',
|
|
706
|
+
title: t('paste'), 'data-i18n-title': 'paste',
|
|
707
|
+
'aria-label': t('paste'), 'data-i18n-aria-label': 'paste', disabled: true}, [
|
|
697
708
|
yui_toolbar_icon('yi-paste'),
|
|
698
709
|
['span',
|
|
699
710
|
{
|
|
@@ -745,9 +756,17 @@ function build_ui(gobj)
|
|
|
745
756
|
type: 'text',
|
|
746
757
|
/* The placeholder is not a text node, so the
|
|
747
758
|
* data-i18n walk cannot reach it — it needs its own
|
|
748
|
-
* key or it stays English for good.
|
|
759
|
+
* key or it stays English for good.
|
|
760
|
+
*
|
|
761
|
+
* And a placeholder is not a NAME: it goes away the
|
|
762
|
+
* moment something is typed, and a reader is not
|
|
763
|
+
* obliged to announce it. The box carries no <label>
|
|
764
|
+
* either, so without the two below it is an unnamed
|
|
765
|
+
* text field. */
|
|
749
766
|
placeholder: t('search'),
|
|
750
|
-
'data-i18n-placeholder': 'search'
|
|
767
|
+
'data-i18n-placeholder': 'search',
|
|
768
|
+
title: t('search'), 'data-i18n-title': 'search',
|
|
769
|
+
'aria-label': t('search'), 'data-i18n-aria-label': 'search'
|
|
751
770
|
}],
|
|
752
771
|
['span', {class: 'icon is-left'}, [
|
|
753
772
|
yui_toolbar_icon('yi-magnifying-glass')
|
|
@@ -776,7 +795,9 @@ function build_ui(gobj)
|
|
|
776
795
|
|
|
777
796
|
if(with_refresh_button) {
|
|
778
797
|
let $refresh = createElement2(
|
|
779
|
-
['button', {class: 'button mr-1',
|
|
798
|
+
['button', {class: 'button mr-1',
|
|
799
|
+
title: t('refresh'), 'data-i18n-title': 'refresh',
|
|
800
|
+
'aria-label': t('refresh'), 'data-i18n-aria-label': 'refresh'}, [
|
|
780
801
|
yui_toolbar_icon('yi-arrows-rotate'),
|
|
781
802
|
['span', {class: 'is-hidden-mobile', i18n: 'refresh', style: 'padding-left:5px;'}, 'refresh']
|
|
782
803
|
], {
|
|
@@ -1485,7 +1506,7 @@ function create_tabulator(gobj)
|
|
|
1485
1506
|
let index = cell.getRow().getPosition();
|
|
1486
1507
|
if(e.target.closest('.edit')) {
|
|
1487
1508
|
e.stopPropagation();
|
|
1488
|
-
|
|
1509
|
+
gobj_send_event(gobj, "EV_EDIT_RECORD", {index: index}, gobj);
|
|
1489
1510
|
} else if(e.target.closest('.remove')) {
|
|
1490
1511
|
e.stopPropagation();
|
|
1491
1512
|
gobj_send_event(gobj, "EV_DELETE_ROWS", {index: index, row: row}, gobj);
|
|
@@ -2263,15 +2284,31 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2263
2284
|
return;
|
|
2264
2285
|
}
|
|
2265
2286
|
|
|
2266
|
-
/*
|
|
2267
|
-
*
|
|
2287
|
+
/* The title says WHAT YOU ARE DOING, and it has to survive a
|
|
2288
|
+
* language change.
|
|
2289
|
+
*
|
|
2290
|
+
* It used to be composed here -- `t("new") + " " + t(topic_name)`
|
|
2291
|
+
* -- and handed to the dialog as its `title`, which the header
|
|
2292
|
+
* renders as an i18n KEY. So the composed sentence was looked up
|
|
2293
|
+
* as a key, found nothing, and rendered as itself: right the first
|
|
2294
|
+
* time, because both halves had been translated already, and
|
|
2295
|
+
* FROZEN in that language for the life of the dialog. Switching
|
|
2296
|
+
* language left "Nuevo usuarios" on an English page.
|
|
2297
|
+
*
|
|
2298
|
+
* The header carries two halves and only one of them can hold a
|
|
2299
|
+
* key: `title_prefix` is DATA and is never translated, `title` is
|
|
2300
|
+
* the KIND and re-translates. So the record id goes in the prefix
|
|
2301
|
+
* -- it is the one part no language touches -- and the kind takes
|
|
2302
|
+
* the key. The topic name leaves the title: it is what the TAB
|
|
2303
|
+
* behind the dialog says, and the form's own fields say it again,
|
|
2304
|
+
* while "new"/"edit"/"view" is what only this header can tell you.
|
|
2305
|
+
* All three are keys this library already uses. */
|
|
2268
2306
|
let pkey = desc.pkey || "id";
|
|
2269
|
-
let title;
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
} else {
|
|
2307
|
+
let title = (mode === "create")? "new" : ((mode === "view")? "view" : "edit");
|
|
2308
|
+
let title_prefix = "";
|
|
2309
|
+
if(mode !== "create") {
|
|
2273
2310
|
let rid = record ? String(record[pkey] ?? "") : "";
|
|
2274
|
-
|
|
2311
|
+
title_prefix = empty_string(rid)? "" : rid;
|
|
2275
2312
|
}
|
|
2276
2313
|
|
|
2277
2314
|
/* "view" is an "update" that writes nothing: same title, same pkey
|
|
@@ -2334,6 +2371,7 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2334
2371
|
priv.form_modal = yui_shell_show_modal(shell, $body, {
|
|
2335
2372
|
dialog: true,
|
|
2336
2373
|
logical_class: "TREEDB_FORM_SHEET",
|
|
2374
|
+
title_prefix: title_prefix,
|
|
2337
2375
|
title: title,
|
|
2338
2376
|
t: t,
|
|
2339
2377
|
before_close: function() {
|
|
@@ -2382,15 +2420,8 @@ function form_may_close(gobj)
|
|
|
2382
2420
|
* a second key to define — and the one that was here had never
|
|
2383
2421
|
* been defined in any locale, which renders as the English
|
|
2384
2422
|
* sentence itself, in every language, for ever. */
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
kw.warning || "all changes will be lost",
|
|
2388
|
-
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2389
|
-
).then(function(answer) {
|
|
2390
|
-
if(answer === "yes" && priv.form_modal) {
|
|
2391
|
-
priv.form_modal.close();
|
|
2392
|
-
}
|
|
2393
|
-
});
|
|
2423
|
+
confirm_then(gobj, kw.warning || "all changes will be lost",
|
|
2424
|
+
{what: "close_form"});
|
|
2394
2425
|
return false;
|
|
2395
2426
|
}
|
|
2396
2427
|
}
|
|
@@ -2416,6 +2447,33 @@ function teardown_form_child(gobj)
|
|
|
2416
2447
|
priv.form_modal = null;
|
|
2417
2448
|
}
|
|
2418
2449
|
|
|
2450
|
+
/************************************************************
|
|
2451
|
+
* Ask, and turn the answer into an EVENT.
|
|
2452
|
+
*
|
|
2453
|
+
* A dialog's promise settles outside the machine: acting from its
|
|
2454
|
+
* `.then` puts the decision -- delete these records, throw these
|
|
2455
|
+
* edits away -- in a callback the `machine` trace never sees, and
|
|
2456
|
+
* in a view that may be gone by the time the person answers.
|
|
2457
|
+
*
|
|
2458
|
+
* `kw` carries an IDENTITY and never an object: the trace dumps
|
|
2459
|
+
* it, and a gobj, a Tabulator row or a DOM node in there is a
|
|
2460
|
+
* circular structure that breaks the very trace the FSM feeds.
|
|
2461
|
+
*
|
|
2462
|
+
* Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
|
|
2463
|
+
************************************************************/
|
|
2464
|
+
function confirm_then(gobj, message, kw)
|
|
2465
|
+
{
|
|
2466
|
+
yui_shell_confirm_yesnocancel(
|
|
2467
|
+
yui_shell_of(gobj), message,
|
|
2468
|
+
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2469
|
+
).then(function(answer) {
|
|
2470
|
+
if(answer !== "yes" || gobj_is_destroying(gobj)) {
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
gobj_send_event(gobj, "EV_CONFIRMED", kw, gobj);
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2419
2477
|
/************************************************************
|
|
2420
2478
|
* Force the dialog down (bypasses the unsaved guard): used after a
|
|
2421
2479
|
* successful save and on teardown. Closing the modal runs its
|
|
@@ -3130,7 +3188,7 @@ function pkey_is_rowid(gobj)
|
|
|
3130
3188
|
* Show the edit "update" form of a row (record)
|
|
3131
3189
|
* internally called from the icon edit at Op column
|
|
3132
3190
|
************************************************************/
|
|
3133
|
-
function show_edit_form(gobj, row
|
|
3191
|
+
function show_edit_form(gobj, row)
|
|
3134
3192
|
{
|
|
3135
3193
|
/* a rowid pkey has no "update": every save appends a new
|
|
3136
3194
|
* instance (timeranger semantics) — open in create mode */
|
|
@@ -3940,50 +3998,81 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
3940
3998
|
return 0;
|
|
3941
3999
|
}
|
|
3942
4000
|
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
3946
|
-
).then(function(answer) {
|
|
3947
|
-
if(answer === "yes") {
|
|
3948
|
-
for(let row of rows) {
|
|
3949
|
-
// TODO why don't send once EV_DELETE_RECORD(S)
|
|
3950
|
-
gobj_publish_event(
|
|
3951
|
-
gobj,
|
|
3952
|
-
"EV_DELETE_RECORD",
|
|
3953
|
-
{
|
|
3954
|
-
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
3955
|
-
record: row
|
|
3956
|
-
}
|
|
3957
|
-
);
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
});
|
|
4001
|
+
confirm_then(gobj, build_delete_question(gobj, rows),
|
|
4002
|
+
{what: "delete_selection"});
|
|
3961
4003
|
|
|
3962
4004
|
} else {
|
|
3963
4005
|
/*----------------------------*
|
|
3964
4006
|
* Delete one row
|
|
3965
4007
|
* {index: , row: }
|
|
3966
4008
|
*----------------------------*/
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
3970
|
-
).then(function(answer) {
|
|
3971
|
-
if(answer === "yes") {
|
|
3972
|
-
gobj_publish_event(
|
|
3973
|
-
gobj,
|
|
3974
|
-
"EV_DELETE_RECORD",
|
|
3975
|
-
{
|
|
3976
|
-
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
3977
|
-
record: kw.row
|
|
3978
|
-
}
|
|
3979
|
-
);
|
|
3980
|
-
}
|
|
3981
|
-
});
|
|
4009
|
+
confirm_then(gobj, build_delete_question(gobj, kw.row),
|
|
4010
|
+
{what: "delete_row", index: kw.index});
|
|
3982
4011
|
}
|
|
3983
4012
|
|
|
3984
4013
|
return 0;
|
|
3985
4014
|
}
|
|
3986
4015
|
|
|
4016
|
+
/************************************************************
|
|
4017
|
+
* {what, ...} -- the person said yes to what confirm_then()
|
|
4018
|
+
* asked. The rows are read again HERE and not carried in the
|
|
4019
|
+
* kw: the kw is plain json, and a Tabulator row is not.
|
|
4020
|
+
************************************************************/
|
|
4021
|
+
function ac_confirmed(gobj, event, kw, src)
|
|
4022
|
+
{
|
|
4023
|
+
let priv = gobj.priv;
|
|
4024
|
+
let what = kw? kw.what : "";
|
|
4025
|
+
|
|
4026
|
+
if(what === "close_form") {
|
|
4027
|
+
if(priv.form_modal) {
|
|
4028
|
+
priv.form_modal.close();
|
|
4029
|
+
}
|
|
4030
|
+
return 0;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
4034
|
+
return -1; /* Error already logged */
|
|
4035
|
+
}
|
|
4036
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
4037
|
+
let topic_name = gobj_read_str_attr(gobj, "topic_name");
|
|
4038
|
+
|
|
4039
|
+
if(what === "delete_selection") {
|
|
4040
|
+
let rows = yui_selected_rows(tabulator);
|
|
4041
|
+
if(!rows.length) {
|
|
4042
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with the selection gone`);
|
|
4043
|
+
return -1;
|
|
4044
|
+
}
|
|
4045
|
+
for(let row of rows) {
|
|
4046
|
+
// TODO why don't send once EV_DELETE_RECORD(S)
|
|
4047
|
+
gobj_publish_event(
|
|
4048
|
+
gobj,
|
|
4049
|
+
"EV_DELETE_RECORD",
|
|
4050
|
+
{topic_name: topic_name, record: row}
|
|
4051
|
+
);
|
|
4052
|
+
}
|
|
4053
|
+
return 0;
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
if(what === "delete_row") {
|
|
4057
|
+
let index = kw.index;
|
|
4058
|
+
let row = (tabulator && typeof index === "number")?
|
|
4059
|
+
tabulator.getRowFromPosition(index) : null;
|
|
4060
|
+
if(!row) {
|
|
4061
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no row: ${index}`);
|
|
4062
|
+
return -1;
|
|
4063
|
+
}
|
|
4064
|
+
gobj_publish_event(
|
|
4065
|
+
gobj,
|
|
4066
|
+
"EV_DELETE_RECORD",
|
|
4067
|
+
{topic_name: topic_name, record: row.getData()}
|
|
4068
|
+
);
|
|
4069
|
+
return 0;
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${what}`);
|
|
4073
|
+
return -1;
|
|
4074
|
+
}
|
|
4075
|
+
|
|
3987
4076
|
/************************************************************
|
|
3988
4077
|
* From internal
|
|
3989
4078
|
************************************************************/
|
|
@@ -4085,12 +4174,15 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
4085
4174
|
if(cols.length === 0) {
|
|
4086
4175
|
publish_treedb_write(gobj, mode, kw);
|
|
4087
4176
|
|
|
4088
|
-
/*
|
|
4089
|
-
*
|
|
4090
|
-
* callback
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4177
|
+
/* We are INSIDE the form child's gobj_publish_event stack, and
|
|
4178
|
+
* closing the dialog tears that child down -- never destroy the
|
|
4179
|
+
* publisher synchronously from a subscriber's callback. So the
|
|
4180
|
+
* close is DEFERRED, and a deferral is not a time:
|
|
4181
|
+
* gobj_post_event() delivers EV_CLOSE_FORM to us on the next
|
|
4182
|
+
* turn of the loop, it drops it if we are being destroyed
|
|
4183
|
+
* meanwhile, and it says so in the `machine` trace. A bare
|
|
4184
|
+
* setTimeout(…, 0) did none of the three. */
|
|
4185
|
+
gobj_post_event(gobj, "EV_CLOSE_FORM", {}, gobj);
|
|
4094
4186
|
return 0;
|
|
4095
4187
|
}
|
|
4096
4188
|
|
|
@@ -4197,6 +4289,16 @@ function publish_treedb_write(gobj, mode, record, jn_files)
|
|
|
4197
4289
|
);
|
|
4198
4290
|
}
|
|
4199
4291
|
|
|
4292
|
+
/************************************************************
|
|
4293
|
+
* The deferred close of the form dialog, posted by the save
|
|
4294
|
+
* from inside the form child's own publish stack.
|
|
4295
|
+
************************************************************/
|
|
4296
|
+
function ac_close_form(gobj, event, kw, src)
|
|
4297
|
+
{
|
|
4298
|
+
close_form_dialog(gobj);
|
|
4299
|
+
return 0;
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4200
4302
|
/************************************************************
|
|
4201
4303
|
* {mode, record, picks} -- the bytes are here now
|
|
4202
4304
|
************************************************************/
|
|
@@ -4260,7 +4362,7 @@ function ac_unselect_rows(gobj, event, kw, src)
|
|
|
4260
4362
|
|
|
4261
4363
|
// WARNING with radio, there is no unselect event.
|
|
4262
4364
|
|
|
4263
|
-
if(gobj_read_bool_attr(gobj, "
|
|
4365
|
+
if(gobj_read_bool_attr(gobj, "broadcast_unselect_rows_event")) {
|
|
4264
4366
|
gobj_publish_event(gobj, event, kw);
|
|
4265
4367
|
}
|
|
4266
4368
|
|
|
@@ -4809,6 +4911,31 @@ function ac_show_record(gobj, event, kw, src)
|
|
|
4809
4911
|
return 0;
|
|
4810
4912
|
}
|
|
4811
4913
|
|
|
4914
|
+
/************************************************************
|
|
4915
|
+
* {index} -- the pencil of the Op column. Same shape as
|
|
4916
|
+
* EV_SHOW_RECORD: the kw carries WHICH row, and the row itself
|
|
4917
|
+
* is resolved here, where the table is.
|
|
4918
|
+
************************************************************/
|
|
4919
|
+
function ac_edit_record(gobj, event, kw, src)
|
|
4920
|
+
{
|
|
4921
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
4922
|
+
let index = kw? kw.index : undefined;
|
|
4923
|
+
if(!tabulator || typeof index !== "number") {
|
|
4924
|
+
log_error(`${gobj_short_name(gobj)}: no row ${index} to edit`);
|
|
4925
|
+
return -1;
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
let row = tabulator.getRowFromPosition(index);
|
|
4929
|
+
if(!row) {
|
|
4930
|
+
log_error(`${gobj_short_name(gobj)}: row ${index} is gone`);
|
|
4931
|
+
return -1;
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
show_edit_form(gobj, row.getData());
|
|
4935
|
+
|
|
4936
|
+
return 0;
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4812
4939
|
/************************************************************
|
|
4813
4940
|
* The reader picked another page size: remember it, so coming back to
|
|
4814
4941
|
* this topic opens where they left it instead of at 200.
|
|
@@ -4948,11 +5075,13 @@ function create_gclass(gclass_name)
|
|
|
4948
5075
|
|
|
4949
5076
|
["EV_EDITION_MODE", ac_edition_mode, null],
|
|
4950
5077
|
["EV_SAVE_RECORD", ac_form_save_record, null],
|
|
5078
|
+
["EV_CLOSE_FORM", ac_close_form, null],
|
|
4951
5079
|
["EV_FILES_READ", ac_files_read, null],
|
|
4952
5080
|
["EV_FILES_FAILED", ac_files_failed, null],
|
|
4953
5081
|
|
|
4954
5082
|
["EV_NEW_ROW", ac_new_row, null],
|
|
4955
5083
|
["EV_DELETE_ROWS", ac_delete_rows, null],
|
|
5084
|
+
["EV_CONFIRMED", ac_confirmed, null],
|
|
4956
5085
|
["EV_SELECT_ROWS", ac_select_rows, null],
|
|
4957
5086
|
["EV_CLEAR_SELECTION", ac_clear_selection, null],
|
|
4958
5087
|
["EV_UNSELECT_ROWS", ac_unselect_rows, null],
|
|
@@ -4978,6 +5107,7 @@ function create_gclass(gclass_name)
|
|
|
4978
5107
|
["EV_OPEN_COLUMNS", ac_open_columns, null],
|
|
4979
5108
|
["EV_PAGE_SIZE_CHANGED", ac_page_size_changed, null],
|
|
4980
5109
|
["EV_SHOW_RECORD", ac_show_record, null],
|
|
5110
|
+
["EV_EDIT_RECORD", ac_edit_record, null],
|
|
4981
5111
|
["EV_TOGGLE_COLUMN", ac_toggle_column, null],
|
|
4982
5112
|
["EV_EXPORT_TABLE", ac_export_table, null],
|
|
4983
5113
|
["EV_SHOW", ac_show, null],
|
|
@@ -4997,10 +5127,12 @@ function create_gclass(gclass_name)
|
|
|
4997
5127
|
|
|
4998
5128
|
["EV_EDITION_MODE", 0],
|
|
4999
5129
|
["EV_SAVE_RECORD", 0],
|
|
5130
|
+
["EV_CLOSE_FORM", 0],
|
|
5000
5131
|
["EV_FILES_READ", 0],
|
|
5001
5132
|
["EV_FILES_FAILED", 0],
|
|
5002
5133
|
["EV_NEW_ROW", 0],
|
|
5003
5134
|
["EV_DELETE_ROWS", 0],
|
|
5135
|
+
["EV_CONFIRMED", 0],
|
|
5004
5136
|
["EV_COPY_ROWS", 0],
|
|
5005
5137
|
["EV_PASTE_ROWS", 0],
|
|
5006
5138
|
["EV_CLEAR_SELECTION", 0],
|
|
@@ -5030,6 +5162,7 @@ function create_gclass(gclass_name)
|
|
|
5030
5162
|
["EV_OPEN_COLUMNS", 0],
|
|
5031
5163
|
["EV_PAGE_SIZE_CHANGED", 0],
|
|
5032
5164
|
["EV_SHOW_RECORD", 0],
|
|
5165
|
+
["EV_EDIT_RECORD", 0],
|
|
5033
5166
|
["EV_TOGGLE_COLUMN", 0],
|
|
5034
5167
|
["EV_EXPORT_TABLE", 0],
|
|
5035
5168
|
|
package/src/yui_row_search.js
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
/***********************************************************************
|
|
2
2
|
* yui_row_search.js
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* IS THIS TERM IN THIS ROW?
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `[
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* había mirado en otro sitio.
|
|
6
|
+
* The search box of a treedb table looked at the row's values with
|
|
7
|
+
* `String(val)`, and a treedb row is not flat: an fkey arrives as a
|
|
8
|
+
* LIST OF OBJECTS `[{id, topic_name, hook_name}]`, and `String()`
|
|
9
|
+
* of that is `"[object Object]"`. So looking for the workshop of
|
|
10
|
+
* a device -- which is where the datum an operator has in mind
|
|
11
|
+
* lives -- never found anything, and the box gave no hint that it
|
|
12
|
+
* had looked somewhere else.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
14
|
+
* THREE RULES, AND THE LAST TWO ARE WHAT MAKE IT USEFUL:
|
|
16
15
|
*
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* - It walks down lists and objects to a short depth: an fkey is
|
|
17
|
+
* two levels down and nothing anybody searches for is deeper.
|
|
18
|
+
* - **Of an fkey only the `id` is looked at.** `topic_name` and
|
|
19
|
+
* `hook_name` are the SAME two words on every row, so looking at
|
|
20
|
+
* them turns the term into a wildcard: searching "devices" would
|
|
21
|
+
* bring the whole topic. The `id` is the only thing that names
|
|
22
|
+
* the linked thing.
|
|
23
|
+
* - **A hook read as its COUNT is not looked at at all.** Since
|
|
24
|
+
* `hook_size` (7.23.163) a topic table loads a hook as
|
|
25
|
+
* `[{"size": N}]` instead of the id of every child, and that
|
|
26
|
+
* number is not a value of the row: matching it made "5" answer
|
|
27
|
+
* every row whose hook happens to hold 5 children, next to the
|
|
28
|
+
* rows that really say 5. A count nobody can see in the cell as
|
|
29
|
+
* text is not what a person typing in a search box is after.
|
|
25
30
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (`__md_treedb__`),
|
|
31
|
+
* Keys that start with `_` are not looked at on any level: they
|
|
32
|
+
* belong to the scaffolding (`_check_box_state_`, `_operation`) or
|
|
33
|
+
* are metadata (`__md_treedb__`), and nobody searches by them.
|
|
29
34
|
*
|
|
30
|
-
* Copyright (c) 2026, ArtGins.
|
|
35
|
+
* Copyright (c) 2024-2026, ArtGins.
|
|
31
36
|
* All Rights Reserved.
|
|
32
37
|
***********************************************************************/
|
|
33
38
|
|
|
@@ -35,7 +40,7 @@ const MAX_DEPTH = 4;
|
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
/***************************************************************
|
|
38
|
-
*
|
|
43
|
+
* An fkey, as `nodes` answers it with `list_dict`.
|
|
39
44
|
***************************************************************/
|
|
40
45
|
function is_fkey_ref(value)
|
|
41
46
|
{
|
|
@@ -47,9 +52,29 @@ function is_fkey_ref(value)
|
|
|
47
52
|
&& ("hook_name" in value);
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
|
|
56
|
+
/***************************************************************
|
|
57
|
+
* A hook read with `hook_size`: the COUNT of its children,
|
|
58
|
+
* `[{"size": N}]`, in the place of the children themselves.
|
|
59
|
+
* The same shape `delete_impact`'s `ref_count()` reads.
|
|
60
|
+
***************************************************************/
|
|
61
|
+
function is_hook_size(value)
|
|
62
|
+
{
|
|
63
|
+
if(!Array.isArray(value) || value.length !== 1) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const first = value[0];
|
|
67
|
+
return !!first
|
|
68
|
+
&& typeof first === "object"
|
|
69
|
+
&& !Array.isArray(first)
|
|
70
|
+
&& typeof first.size === "number"
|
|
71
|
+
&& Object.keys(first).length === 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
50
75
|
/***************************************************************
|
|
51
|
-
*
|
|
52
|
-
*
|
|
76
|
+
* Does `value` hold the term? The term comes ALREADY in lower
|
|
77
|
+
* case: whoever searches lowers it once, not once per cell.
|
|
53
78
|
***************************************************************/
|
|
54
79
|
function value_matches(value, term, depth)
|
|
55
80
|
{
|
|
@@ -59,6 +84,9 @@ function value_matches(value, term, depth)
|
|
|
59
84
|
if(depth > MAX_DEPTH) {
|
|
60
85
|
return false;
|
|
61
86
|
}
|
|
87
|
+
if(is_hook_size(value)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
62
90
|
if(Array.isArray(value)) {
|
|
63
91
|
return value.some((v) => value_matches(v, term, depth + 1));
|
|
64
92
|
}
|
|
@@ -76,8 +104,9 @@ function value_matches(value, term, depth)
|
|
|
76
104
|
return String(value).toLowerCase().includes(term);
|
|
77
105
|
}
|
|
78
106
|
|
|
107
|
+
|
|
79
108
|
/***************************************************************
|
|
80
|
-
*
|
|
109
|
+
* Is the term anywhere in this row?
|
|
81
110
|
***************************************************************/
|
|
82
111
|
function row_matches(row, term)
|
|
83
112
|
{
|
|
@@ -93,4 +122,4 @@ function row_matches(row, term)
|
|
|
93
122
|
}
|
|
94
123
|
|
|
95
124
|
|
|
96
|
-
export {row_matches, value_matches, is_fkey_ref};
|
|
125
|
+
export {row_matches, value_matches, is_fkey_ref, is_hook_size};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {describe, it, expect} from "vitest";
|
|
2
2
|
|
|
3
|
-
import {row_matches, is_fkey_ref} from "./yui_row_search.js";
|
|
3
|
+
import {row_matches, is_fkey_ref, is_hook_size} from "./yui_row_search.js";
|
|
4
4
|
|
|
5
5
|
describe("row_matches", () => {
|
|
6
6
|
const row = {
|
|
@@ -52,6 +52,41 @@ describe("row_matches", () => {
|
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
describe("row_matches, on a table loaded with hook_size", () => {
|
|
56
|
+
/* What a topic table gets since 7.23.163: the hook is the COUNT
|
|
57
|
+
* of its children, not their ids. */
|
|
58
|
+
const row = {
|
|
59
|
+
id: "places-madrid",
|
|
60
|
+
name: "Madrid",
|
|
61
|
+
devices: [{size: 400}],
|
|
62
|
+
__md_treedb__: {topic_name: "places"}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
it("does NOT match the count of a hook", () => {
|
|
66
|
+
/* It did, and "400" then answered every place holding 400
|
|
67
|
+
* devices together with the ones that really say 400. */
|
|
68
|
+
expect(row_matches(row, "400")).toBe(false);
|
|
69
|
+
expect(row_matches(row, "40")).toBe(false);
|
|
70
|
+
expect(row_matches(row, "0")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("still matches the rest of the row", () => {
|
|
74
|
+
expect(row_matches(row, "madrid")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("still walks a hook that DID come with its children", () => {
|
|
78
|
+
/* A node event carries whole hooks whatever the table asked
|
|
79
|
+
* for, so both shapes reach this row. */
|
|
80
|
+
const loaded = {
|
|
81
|
+
id: "places-madrid",
|
|
82
|
+
devices: [
|
|
83
|
+
{id: "E22003089", topic_name: "devices", hook_name: "places"}
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
expect(row_matches(loaded, "e22003089")).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
55
90
|
describe("is_fkey_ref", () => {
|
|
56
91
|
it("wants the three keys", () => {
|
|
57
92
|
expect(is_fkey_ref({id: "a", topic_name: "t", hook_name: "h"})).toBe(true);
|
|
@@ -60,3 +95,18 @@ describe("is_fkey_ref", () => {
|
|
|
60
95
|
expect(is_fkey_ref(null)).toBe(false);
|
|
61
96
|
});
|
|
62
97
|
});
|
|
98
|
+
|
|
99
|
+
describe("is_hook_size", () => {
|
|
100
|
+
it("wants the one-element list holding only a numeric size", () => {
|
|
101
|
+
expect(is_hook_size([{size: 5}])).toBe(true);
|
|
102
|
+
expect(is_hook_size([{size: 0}])).toBe(true);
|
|
103
|
+
expect(is_hook_size([])).toBe(false);
|
|
104
|
+
expect(is_hook_size([{size: 5}, {size: 6}])).toBe(false);
|
|
105
|
+
expect(is_hook_size([{size: "5"}])).toBe(false);
|
|
106
|
+
expect(is_hook_size({size: 5})).toBe(false);
|
|
107
|
+
/* A record that happens to carry a `size` column is data, and
|
|
108
|
+
* is searched like any other. */
|
|
109
|
+
expect(is_hook_size([{id: "a", size: 5}])).toBe(false);
|
|
110
|
+
expect(is_hook_size(null)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|