@yuneta/gobj-ui 7.0.1 → 7.2.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 +56 -0
- package/dist/gobj-ui.cjs.js +504 -101
- package/dist/gobj-ui.es.js +504 -101
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +113 -0
- package/src/c_yui_treedb_graph.js +93 -2
- package/src/c_yui_treedb_topic_with_form.js +265 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuneta/gobj-ui",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/gobj-ui.cjs.js",
|
|
6
6
|
"module": "dist/gobj-ui.es.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"src/",
|
|
21
21
|
"vite-plugin-yuneta-html.js"
|
|
22
22
|
],
|
|
23
|
-
"description": "Yuneta UI Library
|
|
23
|
+
"description": "Yuneta UI Library — the active line: declarative shell (C_YUI_SHELL+NAV+PAGER+WIZARD) + windows, TreeDB views, forms, charts and maps. Consumed by wattyzer and the yunetas JS yunos (gui_agent, gui_treedb). The frozen legacy GClass GUI stack (C_YUI_MAIN/TABS/ROUTING) lives on the v1 branch, npm dist-tag 'legacy' (consumed by estadodelaire and hidraulia).",
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "vite build",
|
|
26
26
|
"test": "vitest run",
|
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -280,6 +280,7 @@ let PRIVATE_DATA = {
|
|
|
280
280
|
_focus_topic: null, // topic currently focused (EV_FOCUS_TOPIC)
|
|
281
281
|
_focus_ids: [], // node ids carrying the focus 'active' state
|
|
282
282
|
_pending_focus_topic: null, // focus requested before data was loaded
|
|
283
|
+
_pending_find: null, // find requested before data was loaded
|
|
283
284
|
};
|
|
284
285
|
|
|
285
286
|
let __gclass__ = null;
|
|
@@ -2127,6 +2128,89 @@ function graph_focus_topic(gobj, topic)
|
|
|
2127
2128
|
}
|
|
2128
2129
|
}
|
|
2129
2130
|
|
|
2131
|
+
/************************************************************
|
|
2132
|
+
* Find the nodes whose name, id or topic contains `term`, highlight
|
|
2133
|
+
* them with the same amber 'active' state the topic focus uses, and
|
|
2134
|
+
* centre the viewport on them. Returns how many matched, which is the
|
|
2135
|
+
* half the toolbar needs: a search that finds nothing must SAY so,
|
|
2136
|
+
* because a graph that did not move is also what a graph looks like
|
|
2137
|
+
* when the match is off-screen.
|
|
2138
|
+
*
|
|
2139
|
+
* It shares the highlight slot with graph_focus_topic on purpose —
|
|
2140
|
+
* two amber sets at once would say nothing about either. An empty
|
|
2141
|
+
* term clears it. Matching reads the node's LABEL and not only its
|
|
2142
|
+
* id: on a topic keyed by rowid or by a qualified path, the id is a
|
|
2143
|
+
* counter and the name a human knows is elsewhere (see node_label).
|
|
2144
|
+
************************************************************/
|
|
2145
|
+
function graph_find_nodes(gobj, term)
|
|
2146
|
+
{
|
|
2147
|
+
let priv = gobj.priv;
|
|
2148
|
+
let graph = priv.graph;
|
|
2149
|
+
if(!graph || !priv.graph_rendered) {
|
|
2150
|
+
priv._pending_find = term;
|
|
2151
|
+
return 0;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
let data = {};
|
|
2155
|
+
try {
|
|
2156
|
+
data = graph.getData() || {};
|
|
2157
|
+
} catch(e) {
|
|
2158
|
+
log_error(`${gobj_short_name(gobj)}: graph.getData() failed: ${e}`);
|
|
2159
|
+
return 0;
|
|
2160
|
+
}
|
|
2161
|
+
let nodes = is_array(data.nodes) ? data.nodes : [];
|
|
2162
|
+
|
|
2163
|
+
let states = {};
|
|
2164
|
+
for(let id of (priv._focus_ids || [])) {
|
|
2165
|
+
states[id] = [];
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
let ids = [];
|
|
2169
|
+
if(!empty_string(term)) {
|
|
2170
|
+
let needle = String(term).toLowerCase();
|
|
2171
|
+
for(let n of nodes) {
|
|
2172
|
+
if(!n || !n.data) {
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
let record = n.data.record || {};
|
|
2176
|
+
let label = "";
|
|
2177
|
+
try {
|
|
2178
|
+
label = node_label(n.data.desc || {}, record) || "";
|
|
2179
|
+
} catch(e) {
|
|
2180
|
+
label = "";
|
|
2181
|
+
}
|
|
2182
|
+
let haystack = [
|
|
2183
|
+
label,
|
|
2184
|
+
record.id,
|
|
2185
|
+
n.data.topic_name
|
|
2186
|
+
].filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
2187
|
+
|
|
2188
|
+
if(haystack.includes(needle)) {
|
|
2189
|
+
ids.push(n.id);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
for(let id of ids) {
|
|
2193
|
+
states[id] = ['active'];
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
priv._focus_ids = ids;
|
|
2198
|
+
priv._focus_topic = null; /* a find takes over the highlight */
|
|
2199
|
+
|
|
2200
|
+
try {
|
|
2201
|
+
if(typeof graph.setElementState === "function") {
|
|
2202
|
+
graph.setElementState(states);
|
|
2203
|
+
}
|
|
2204
|
+
if(ids.length && typeof graph.focusElement === "function") {
|
|
2205
|
+
graph.focusElement(ids);
|
|
2206
|
+
}
|
|
2207
|
+
} catch(e) {
|
|
2208
|
+
log_error(`${gobj_short_name(gobj)}: find apply failed: ${e}`);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
return ids.length;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2130
2214
|
async function graph_zoom_in(gobj)
|
|
2131
2215
|
{
|
|
2132
2216
|
let priv = gobj.priv;
|
|
@@ -5546,6 +5630,11 @@ function ac_load_data(gobj, event, kw, src)
|
|
|
5546
5630
|
priv._pending_focus_topic = null;
|
|
5547
5631
|
graph_focus_topic(gobj, ft);
|
|
5548
5632
|
}
|
|
5633
|
+
if(priv._pending_find !== null) {
|
|
5634
|
+
let term = priv._pending_find;
|
|
5635
|
+
priv._pending_find = null;
|
|
5636
|
+
publish_find_result(gobj, term, graph_find_nodes(gobj, term));
|
|
5637
|
+
}
|
|
5549
5638
|
});
|
|
5550
5639
|
});
|
|
5551
5640
|
});
|
|
@@ -5891,6 +5980,27 @@ function ac_focus_topic(gobj, event, kw, src)
|
|
|
5891
5980
|
return 0;
|
|
5892
5981
|
}
|
|
5893
5982
|
|
|
5983
|
+
/************************************************************
|
|
5984
|
+
* Tell whoever asked how many nodes the term matched. The count is
|
|
5985
|
+
* the answer to the question the box asks; without it "nothing moved"
|
|
5986
|
+
* and "nothing matched" look the same.
|
|
5987
|
+
************************************************************/
|
|
5988
|
+
function publish_find_result(gobj, term, matches)
|
|
5989
|
+
{
|
|
5990
|
+
gobj_publish_event(gobj, "EV_FIND_RESULT", {term: term, matches: matches});
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
/************************************************************
|
|
5994
|
+
*
|
|
5995
|
+
************************************************************/
|
|
5996
|
+
function ac_find_nodes(gobj, event, kw, src)
|
|
5997
|
+
{
|
|
5998
|
+
let term = (kw && kw.text) || "";
|
|
5999
|
+
let matches = graph_find_nodes(gobj, term);
|
|
6000
|
+
publish_find_result(gobj, term, matches);
|
|
6001
|
+
return 0;
|
|
6002
|
+
}
|
|
6003
|
+
|
|
5894
6004
|
function ac_center(gobj, event, kw, src)
|
|
5895
6005
|
{
|
|
5896
6006
|
graph_center(gobj);
|
|
@@ -6254,6 +6364,7 @@ function create_gclass(gclass_name)
|
|
|
6254
6364
|
["EV_ZOOM_RESET", ac_zoom_reset, null],
|
|
6255
6365
|
["EV_AUTO_FIT", ac_auto_fit, null],
|
|
6256
6366
|
["EV_FOCUS_TOPIC", ac_focus_topic, null],
|
|
6367
|
+
["EV_FIND_NODES", ac_find_nodes, null],
|
|
6257
6368
|
["EV_CENTER", ac_center, null],
|
|
6258
6369
|
["EV_FULLSCREEN", ac_fullscreen, null],
|
|
6259
6370
|
["EV_SET_LAYOUT", ac_set_layout, null],
|
|
@@ -6297,6 +6408,8 @@ function create_gclass(gclass_name)
|
|
|
6297
6408
|
["EV_ZOOM_RESET", 0],
|
|
6298
6409
|
["EV_AUTO_FIT", 0],
|
|
6299
6410
|
["EV_FOCUS_TOPIC", 0],
|
|
6411
|
+
["EV_FIND_NODES", 0],
|
|
6412
|
+
["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
|
|
6300
6413
|
["EV_CENTER", 0],
|
|
6301
6414
|
["EV_FULLSCREEN", 0],
|
|
6302
6415
|
["EV_SET_LAYOUT", 0],
|
|
@@ -504,9 +504,51 @@ function make_toolbar(gobj)
|
|
|
504
504
|
];
|
|
505
505
|
|
|
506
506
|
/*
|
|
507
|
-
* Center
|
|
507
|
+
* Center: find a node.
|
|
508
|
+
*
|
|
509
|
+
* A graph of a few hundred records has no other way in: the only way
|
|
510
|
+
* to locate one was to read every card. The box highlights every
|
|
511
|
+
* match with the same amber the topic focus uses and centres the
|
|
512
|
+
* viewport on them, and it SAYS how many it found — a graph that did
|
|
513
|
+
* not move looks the same whether nothing matched or the match was
|
|
514
|
+
* already on screen.
|
|
508
515
|
*/
|
|
509
|
-
let center_items = [
|
|
516
|
+
let center_items = [
|
|
517
|
+
['div', {class: 'GRAPH_FIND control has-icons-left',
|
|
518
|
+
style: 'margin-right:.5rem; max-width:12rem; min-width:7rem;'}, [
|
|
519
|
+
['input', {
|
|
520
|
+
class: 'GRAPH_FIND_INPUT input',
|
|
521
|
+
type: 'text',
|
|
522
|
+
/* A placeholder is not a text node, so the data-i18n walk
|
|
523
|
+
* cannot reach it: it needs its own key. */
|
|
524
|
+
placeholder: t('search'),
|
|
525
|
+
'data-i18n-placeholder': 'search',
|
|
526
|
+
'aria-label': t('search'),
|
|
527
|
+
'data-i18n-aria-label': 'search'
|
|
528
|
+
}],
|
|
529
|
+
['span', {class: 'icon is-left'}, [
|
|
530
|
+
['i', {class: 'yi-magnifying-glass'}]
|
|
531
|
+
]]
|
|
532
|
+
], {
|
|
533
|
+
input: (evt) => {
|
|
534
|
+
evt.stopPropagation();
|
|
535
|
+
gobj_send_event(
|
|
536
|
+
gobj,
|
|
537
|
+
"EV_FIND_NODES",
|
|
538
|
+
{text: evt.target.value.trim()},
|
|
539
|
+
gobj
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}],
|
|
543
|
+
/* Two spans, not one string: the number is DATA and "matches" is
|
|
544
|
+
* the word, so a language switch re-translates the half that is a
|
|
545
|
+
* word. Spaced with CSS because createElement2 trims text nodes. */
|
|
546
|
+
['div', {class: 'GRAPH_FIND_RESULT is-hidden is-flex is-align-items-center',
|
|
547
|
+
style: 'gap:.3rem; margin-right:.5rem; font-size:.85rem;'}, [
|
|
548
|
+
['span', {class: 'GRAPH_FIND_COUNT'}, ''],
|
|
549
|
+
['span', {i18n: 'matches'}, 'matches']
|
|
550
|
+
]]
|
|
551
|
+
];
|
|
510
552
|
|
|
511
553
|
/*
|
|
512
554
|
* Right, fill in set_mode
|
|
@@ -2129,6 +2171,51 @@ function ac_set_operation_mode(gobj, event, kw, src)
|
|
|
2129
2171
|
* tree, which centres + highlights that topic's nodes (deferring
|
|
2130
2172
|
* until its data is loaded).
|
|
2131
2173
|
************************************************************/
|
|
2174
|
+
/************************************************************
|
|
2175
|
+
* Forward the find down to the graph child.
|
|
2176
|
+
************************************************************/
|
|
2177
|
+
function ac_find_nodes(gobj, event, kw, src)
|
|
2178
|
+
{
|
|
2179
|
+
let priv = gobj.priv;
|
|
2180
|
+
if(priv.gobj_nodes_tree) {
|
|
2181
|
+
gobj_send_event(
|
|
2182
|
+
priv.gobj_nodes_tree,
|
|
2183
|
+
"EV_FIND_NODES",
|
|
2184
|
+
{text: (kw && kw.text) || ""},
|
|
2185
|
+
gobj
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
return 0;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/************************************************************
|
|
2192
|
+
* How many nodes the term matched, from the graph child. Zero with a
|
|
2193
|
+
* term typed is an ANSWER and is shown; an empty box shows nothing.
|
|
2194
|
+
************************************************************/
|
|
2195
|
+
function ac_find_result(gobj, event, kw, src)
|
|
2196
|
+
{
|
|
2197
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
2198
|
+
if(!$container) {
|
|
2199
|
+
return 0;
|
|
2200
|
+
}
|
|
2201
|
+
let $result = $container.querySelector(".GRAPH_FIND_RESULT");
|
|
2202
|
+
let $count = $container.querySelector(".GRAPH_FIND_COUNT");
|
|
2203
|
+
if(!$result || !$count) {
|
|
2204
|
+
return 0;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
let term = (kw && kw.term) || "";
|
|
2208
|
+
if(!term) {
|
|
2209
|
+
$result.classList.add("is-hidden");
|
|
2210
|
+
$count.textContent = "";
|
|
2211
|
+
return 0;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
$count.textContent = String((kw && kw.matches) || 0);
|
|
2215
|
+
$result.classList.remove("is-hidden");
|
|
2216
|
+
return 0;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2132
2219
|
function ac_set_focus_topic(gobj, event, kw, src)
|
|
2133
2220
|
{
|
|
2134
2221
|
let priv = gobj.priv;
|
|
@@ -2228,6 +2315,8 @@ function create_gclass(gclass_name)
|
|
|
2228
2315
|
["EV_SET_LAYOUT", ac_set_layout, null],
|
|
2229
2316
|
["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
|
|
2230
2317
|
["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
|
|
2318
|
+
["EV_FIND_NODES", ac_find_nodes, null],
|
|
2319
|
+
["EV_FIND_RESULT", ac_find_result, null],
|
|
2231
2320
|
["EV_SHOW", ac_show, null],
|
|
2232
2321
|
["EV_HIDE", ac_hide, null],
|
|
2233
2322
|
["EV_TRANSPORT_STATE", ac_transport_state, null],
|
|
@@ -2262,6 +2351,8 @@ function create_gclass(gclass_name)
|
|
|
2262
2351
|
["EV_SET_LAYOUT", 0],
|
|
2263
2352
|
["EV_SET_OPERATION_MODE", 0],
|
|
2264
2353
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
2354
|
+
["EV_FIND_NODES", 0],
|
|
2355
|
+
["EV_FIND_RESULT", 0],
|
|
2265
2356
|
["EV_OPERATION_MODE_CHANGED",
|
|
2266
2357
|
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2267
2358
|
["EV_RECORD_WRITTEN",
|
|
@@ -108,6 +108,9 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button
|
|
|
108
108
|
SDATA(data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
|
|
109
109
|
SDATA(data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
|
|
110
110
|
SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
|
|
111
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
|
|
112
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
|
|
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)"),
|
|
111
114
|
SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
|
|
112
115
|
|
|
113
116
|
SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
|
|
@@ -496,6 +499,8 @@ function build_ui(gobj)
|
|
|
496
499
|
let with_refresh_button = gobj_read_bool_attr(gobj, "with_refresh_button");
|
|
497
500
|
let with_search_button = gobj_read_bool_attr(gobj, "with_search_button");
|
|
498
501
|
let with_schema_button = gobj_read_bool_attr(gobj, "with_schema_button");
|
|
502
|
+
let with_columns_button = gobj_read_bool_attr(gobj, "with_columns_button");
|
|
503
|
+
let with_export_button = gobj_read_bool_attr(gobj, "with_export_button");
|
|
499
504
|
|
|
500
505
|
if(with_search_button) {
|
|
501
506
|
let search_id = `${toolbar_id}_search`;
|
|
@@ -521,27 +526,16 @@ function build_ui(gobj)
|
|
|
521
526
|
* filter below re-runs (empty term → clearFilter). */
|
|
522
527
|
attach_clear($search_box, $search_input);
|
|
523
528
|
if($search_input) {
|
|
529
|
+
/* The DOM handler's only job is to turn the keystroke into an
|
|
530
|
+
* event: searching is a user action, so it crosses the FSM and
|
|
531
|
+
* shows up in the `machine` trace like every other one. */
|
|
524
532
|
$search_input.addEventListener('input', (event) => {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
tabulator.setFilter(function(data) {
|
|
532
|
-
return Object.entries(data).some(([key, val]) => {
|
|
533
|
-
if(key.startsWith('_')) {
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
if(val === null || val === undefined) {
|
|
537
|
-
return false;
|
|
538
|
-
}
|
|
539
|
-
return String(val).toLowerCase().includes(searchText);
|
|
540
|
-
});
|
|
541
|
-
});
|
|
542
|
-
} else {
|
|
543
|
-
tabulator.clearFilter();
|
|
544
|
-
}
|
|
533
|
+
gobj_send_event(
|
|
534
|
+
gobj,
|
|
535
|
+
"EV_SEARCH",
|
|
536
|
+
{text: event.target.value.trim()},
|
|
537
|
+
gobj
|
|
538
|
+
);
|
|
545
539
|
});
|
|
546
540
|
}
|
|
547
541
|
$view_toolbar.appendChild($search_box);
|
|
@@ -583,6 +577,48 @@ function build_ui(gobj)
|
|
|
583
577
|
$view_toolbar.appendChild($schema);
|
|
584
578
|
}
|
|
585
579
|
|
|
580
|
+
if(with_columns_button) {
|
|
581
|
+
/* A topic with a dozen columns is wider than the screen, and until
|
|
582
|
+
* now the reader could not decide WHICH of them to keep: the table
|
|
583
|
+
* simply scrolled sideways for ever. */
|
|
584
|
+
let $columns = createElement2(
|
|
585
|
+
['button', {class: 'TREEDB_TABLE_COLUMNS button mr-1',
|
|
586
|
+
title: t('choose the columns to show'),
|
|
587
|
+
'data-i18n-title': 'choose the columns to show',
|
|
588
|
+
'aria-label': t('columns'), 'data-i18n-aria-label': 'columns'}, [
|
|
589
|
+
['i', {class: 'yi-table'}],
|
|
590
|
+
['span', {class: 'is-hidden-mobile', i18n: 'columns', style: 'padding-left:5px;'}, 'columns']
|
|
591
|
+
], {
|
|
592
|
+
'click': (event) => {
|
|
593
|
+
event.stopPropagation();
|
|
594
|
+
gobj_send_event(gobj, "EV_OPEN_COLUMNS", {}, gobj);
|
|
595
|
+
}
|
|
596
|
+
}]
|
|
597
|
+
);
|
|
598
|
+
$view_toolbar.appendChild($columns);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if(with_export_button) {
|
|
602
|
+
/* What the TABLE holds, filters and column choice applied — not the
|
|
603
|
+
* topic. A server-side dump of every node is not something this view
|
|
604
|
+
* can stream, and the title says so. */
|
|
605
|
+
let $export = createElement2(
|
|
606
|
+
['button', {class: 'TREEDB_TABLE_EXPORT button mr-1',
|
|
607
|
+
title: t('download the rows loaded in this table as csv'),
|
|
608
|
+
'data-i18n-title': 'download the rows loaded in this table as csv',
|
|
609
|
+
'aria-label': t('export'), 'data-i18n-aria-label': 'export'}, [
|
|
610
|
+
['i', {class: 'yi-download'}],
|
|
611
|
+
['span', {class: 'is-hidden-mobile', i18n: 'export', style: 'padding-left:5px;'}, 'export']
|
|
612
|
+
], {
|
|
613
|
+
'click': (event) => {
|
|
614
|
+
event.stopPropagation();
|
|
615
|
+
gobj_send_event(gobj, "EV_EXPORT_TABLE", {}, gobj);
|
|
616
|
+
}
|
|
617
|
+
}]
|
|
618
|
+
);
|
|
619
|
+
$view_toolbar.appendChild($export);
|
|
620
|
+
}
|
|
621
|
+
|
|
586
622
|
/*----------------------------------------------*
|
|
587
623
|
* Layout Schema
|
|
588
624
|
*----------------------------------------------*/
|
|
@@ -693,6 +729,7 @@ function create_tabulator(gobj)
|
|
|
693
729
|
{
|
|
694
730
|
let table_id = gobj_read_str_attr(gobj, "table_id");
|
|
695
731
|
let desc = gobj_read_attr(gobj, "desc");
|
|
732
|
+
let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
|
|
696
733
|
|
|
697
734
|
let columns = [];
|
|
698
735
|
|
|
@@ -773,6 +810,58 @@ function create_tabulator(gobj)
|
|
|
773
810
|
);
|
|
774
811
|
}
|
|
775
812
|
|
|
813
|
+
/* A filter box per column, on the columns where a text/number match
|
|
814
|
+
* MEANS something. A hook holds children, a dict holds a subtree and a
|
|
815
|
+
* date cell shows a formatted string over an epoch number — matching
|
|
816
|
+
* the raw value there answers a question nobody asked, so those columns
|
|
817
|
+
* get no box rather than a box that lies. An fkey does get one: "which
|
|
818
|
+
* rows point at X" is the question fkey columns exist to answer, and its
|
|
819
|
+
* value is stringified first because a fkey can arrive as a ref string,
|
|
820
|
+
* a list of them or a dict.
|
|
821
|
+
*/
|
|
822
|
+
const FILTERABLE = [
|
|
823
|
+
"string", "integer", "real", "rowid", "uuid", "qualified",
|
|
824
|
+
"email", "url", "tel", "id", "hex", "currency", "percent"
|
|
825
|
+
];
|
|
826
|
+
|
|
827
|
+
function fkey_matches(term, value)
|
|
828
|
+
{
|
|
829
|
+
if(term === "" || term === null || term === undefined) {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
if(value === null || value === undefined) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
let text = (typeof value === "string")? value : JSON.stringify(value);
|
|
836
|
+
return text.toLowerCase().includes(String(term).toLowerCase());
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function apply_header_filter(colDef, field_desc)
|
|
840
|
+
{
|
|
841
|
+
switch(field_desc.type) {
|
|
842
|
+
case "boolean":
|
|
843
|
+
colDef.headerFilter = "tickCross";
|
|
844
|
+
colDef.headerFilterParams = {tristate: true};
|
|
845
|
+
break;
|
|
846
|
+
case "enum":
|
|
847
|
+
colDef.headerFilter = "list";
|
|
848
|
+
colDef.headerFilterParams = {
|
|
849
|
+
values: field_desc.enum_list || [],
|
|
850
|
+
clearable: true
|
|
851
|
+
};
|
|
852
|
+
break;
|
|
853
|
+
case "fkey":
|
|
854
|
+
colDef.headerFilter = "input";
|
|
855
|
+
colDef.headerFilterFunc = fkey_matches;
|
|
856
|
+
break;
|
|
857
|
+
default:
|
|
858
|
+
if(FILTERABLE.includes(field_desc.type)) {
|
|
859
|
+
colDef.headerFilter = "input";
|
|
860
|
+
}
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
776
865
|
for (let i = 0; i < desc.cols.length; i++) {
|
|
777
866
|
let col = desc.cols[i];
|
|
778
867
|
if(!col.id || col.id[0]==='_') {
|
|
@@ -866,6 +955,9 @@ function create_tabulator(gobj)
|
|
|
866
955
|
hozAlign: hozAlign,
|
|
867
956
|
formatter: colFormatter,
|
|
868
957
|
};
|
|
958
|
+
if(with_header_filters) {
|
|
959
|
+
apply_header_filter(colDef, field_desc);
|
|
960
|
+
}
|
|
869
961
|
if(vertAlign) {
|
|
870
962
|
colDef.vertAlign = vertAlign;
|
|
871
963
|
}
|
|
@@ -2719,6 +2811,151 @@ function ac_show_schema(gobj, event, kw, src)
|
|
|
2719
2811
|
return 0;
|
|
2720
2812
|
}
|
|
2721
2813
|
|
|
2814
|
+
/************************************************************
|
|
2815
|
+
* Filter the loaded rows by a term matched against every non-internal
|
|
2816
|
+
* field. `clearFilter()` with no argument drops only THIS filter: the
|
|
2817
|
+
* per-column header filters are a separate layer and survive, so a
|
|
2818
|
+
* cleared search box does not silently undo them.
|
|
2819
|
+
************************************************************/
|
|
2820
|
+
function ac_search(gobj, event, kw, src)
|
|
2821
|
+
{
|
|
2822
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2823
|
+
if(!tabulator) {
|
|
2824
|
+
log_error(`${gobj_short_name(gobj)}: no table to search`);
|
|
2825
|
+
return -1;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
let term = ((kw && kw.text) || "").toLowerCase();
|
|
2829
|
+
if(!term) {
|
|
2830
|
+
tabulator.clearFilter();
|
|
2831
|
+
return 0;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
tabulator.setFilter(function(data) {
|
|
2835
|
+
return Object.entries(data).some(([key, val]) => {
|
|
2836
|
+
if(key.startsWith('_')) {
|
|
2837
|
+
return false;
|
|
2838
|
+
}
|
|
2839
|
+
if(val === null || val === undefined) {
|
|
2840
|
+
return false;
|
|
2841
|
+
}
|
|
2842
|
+
return String(val).toLowerCase().includes(term);
|
|
2843
|
+
});
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
return 0;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
/************************************************************
|
|
2850
|
+
* Which columns the table shows: one checkbox per column, the current
|
|
2851
|
+
* visibility ticked. The internal columns (`_check_box_state_`,
|
|
2852
|
+
* `_operation`) are not offered — they are chrome, not data.
|
|
2853
|
+
************************************************************/
|
|
2854
|
+
function ac_open_columns(gobj, event, kw, src)
|
|
2855
|
+
{
|
|
2856
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2857
|
+
if(!tabulator) {
|
|
2858
|
+
log_error(`${gobj_short_name(gobj)}: no table, no columns to choose`);
|
|
2859
|
+
return -1;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
let shell = yui_shell_of(gobj);
|
|
2863
|
+
if(!shell) {
|
|
2864
|
+
log_error(`${gobj_short_name(gobj)}: no shell, cannot open the column chooser`);
|
|
2865
|
+
return -1;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
let $list = createElement2(['div', {class: 'TREEDB_COLUMNS_LIST'}]);
|
|
2869
|
+
let columns = tabulator.getColumns();
|
|
2870
|
+
for(let i = 0; i < columns.length; i++) {
|
|
2871
|
+
let column = columns[i];
|
|
2872
|
+
let field = column.getField();
|
|
2873
|
+
if(!field || field[0] === '_') {
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2876
|
+
let $cb = createElement2(['input', {type: 'checkbox', class: 'mr-2'}]);
|
|
2877
|
+
$cb.checked = column.isVisible();
|
|
2878
|
+
$cb.addEventListener('change', () => {
|
|
2879
|
+
gobj_send_event(
|
|
2880
|
+
gobj,
|
|
2881
|
+
"EV_TOGGLE_COLUMN",
|
|
2882
|
+
{field: field, visible: $cb.checked},
|
|
2883
|
+
gobj
|
|
2884
|
+
);
|
|
2885
|
+
});
|
|
2886
|
+
$list.appendChild(createElement2(
|
|
2887
|
+
['label', {class: 'checkbox is-block mb-1 TREEDB_COLUMN_ITEM'}, [
|
|
2888
|
+
$cb,
|
|
2889
|
+
['span', {class: 'ml-2'}, column.getDefinition().title || field]
|
|
2890
|
+
]]
|
|
2891
|
+
));
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
yui_shell_show_modal(shell, $list, {
|
|
2895
|
+
dialog: true,
|
|
2896
|
+
logical_class: "TREEDB_COLUMNS_DIALOG",
|
|
2897
|
+
title_prefix: gobj_read_str_attr(gobj, "topic_name"),
|
|
2898
|
+
title: "columns",
|
|
2899
|
+
t: t
|
|
2900
|
+
});
|
|
2901
|
+
|
|
2902
|
+
return 0;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
/************************************************************
|
|
2906
|
+
* Show / hide one column.
|
|
2907
|
+
************************************************************/
|
|
2908
|
+
function ac_toggle_column(gobj, event, kw, src)
|
|
2909
|
+
{
|
|
2910
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2911
|
+
let field = (kw && kw.field) || "";
|
|
2912
|
+
if(!tabulator || !field) {
|
|
2913
|
+
log_error(`${gobj_short_name(gobj)}: no column '${field}' to toggle`);
|
|
2914
|
+
return -1;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
try {
|
|
2918
|
+
if(kw.visible) {
|
|
2919
|
+
tabulator.showColumn(field);
|
|
2920
|
+
} else {
|
|
2921
|
+
tabulator.hideColumn(field);
|
|
2922
|
+
}
|
|
2923
|
+
} catch(e) {
|
|
2924
|
+
log_error(`${gobj_short_name(gobj)}: cannot toggle column '${field}': ${e}`);
|
|
2925
|
+
return -1;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
return 0;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
/************************************************************
|
|
2932
|
+
* Download what the table HOLDS as CSV: the loaded rows, the visible
|
|
2933
|
+
* columns, search and header filters applied — which is what the reader
|
|
2934
|
+
* is looking at. Not the topic: a server-side dump of every node is not
|
|
2935
|
+
* something this view can stream.
|
|
2936
|
+
************************************************************/
|
|
2937
|
+
function ac_export_table(gobj, event, kw, src)
|
|
2938
|
+
{
|
|
2939
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2940
|
+
if(!tabulator) {
|
|
2941
|
+
log_error(`${gobj_short_name(gobj)}: no table to export`);
|
|
2942
|
+
return -1;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
let name = `${gobj_read_str_attr(gobj, "treedb_name")}-` +
|
|
2946
|
+
`${gobj_read_str_attr(gobj, "topic_name")}.csv`;
|
|
2947
|
+
name = name.replace(/[^\w.\-]+/g, "_");
|
|
2948
|
+
|
|
2949
|
+
try {
|
|
2950
|
+
tabulator.download("csv", name);
|
|
2951
|
+
} catch(e) {
|
|
2952
|
+
log_error(`${gobj_short_name(gobj)}: CSV export failed: ${e}`);
|
|
2953
|
+
return -1;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
return 0;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2722
2959
|
/************************************************************
|
|
2723
2960
|
* {
|
|
2724
2961
|
* href: href
|
|
@@ -2795,6 +3032,10 @@ function create_gclass(gclass_name)
|
|
|
2795
3032
|
["EV_CHANGE_LOCALE", ac_change_locale, null],
|
|
2796
3033
|
["EV_REFRESH", ac_refresh, null],
|
|
2797
3034
|
["EV_SHOW_SCHEMA", ac_show_schema, null],
|
|
3035
|
+
["EV_SEARCH", ac_search, null],
|
|
3036
|
+
["EV_OPEN_COLUMNS", ac_open_columns, null],
|
|
3037
|
+
["EV_TOGGLE_COLUMN", ac_toggle_column, null],
|
|
3038
|
+
["EV_EXPORT_TABLE", ac_export_table, null],
|
|
2798
3039
|
["EV_SHOW", ac_show, null],
|
|
2799
3040
|
["EV_HIDE", ac_hide, null]
|
|
2800
3041
|
]]
|
|
@@ -2823,6 +3064,10 @@ function create_gclass(gclass_name)
|
|
|
2823
3064
|
["EV_CHANGE_LOCALE", 0],
|
|
2824
3065
|
["EV_REFRESH", 0],
|
|
2825
3066
|
["EV_SHOW_SCHEMA", 0],
|
|
3067
|
+
["EV_SEARCH", 0],
|
|
3068
|
+
["EV_OPEN_COLUMNS", 0],
|
|
3069
|
+
["EV_TOGGLE_COLUMN", 0],
|
|
3070
|
+
["EV_EXPORT_TABLE", 0],
|
|
2826
3071
|
|
|
2827
3072
|
["EV_CREATE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],
|
|
2828
3073
|
["EV_UPDATE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],
|