@yuneta/gobj-ui 3.0.0 → 4.0.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 +324 -6
- package/dist/gobj-ui.cjs.js +10891 -5006
- package/dist/gobj-ui.es.js +10915 -5064
- package/index.js +45 -1
- package/package.json +4 -4
- package/src/c_g6_nodes_tree.js +128 -40
- package/src/c_yui_form.js +112 -53
- package/src/c_yui_gobj_tree_js.js +58 -36
- package/src/c_yui_json.css +139 -0
- package/src/c_yui_json.js +928 -0
- package/src/c_yui_json_graph.js +110 -20
- package/src/c_yui_map.js +18 -36
- package/src/c_yui_nav.js +6 -9
- package/src/c_yui_period.css +184 -0
- package/src/c_yui_period.js +1441 -0
- package/src/c_yui_shell.css +33 -0
- package/src/c_yui_shell.js +672 -111
- package/src/c_yui_treedb_graph.js +639 -37
- package/src/c_yui_treedb_schema.js +478 -0
- package/src/c_yui_treedb_topic_with_form.css +7 -42
- package/src/c_yui_treedb_topic_with_form.js +152 -103
- package/src/c_yui_treedb_topics.css +73 -0
- package/src/c_yui_treedb_topics.js +1070 -29
- package/src/c_yui_window.js +180 -97
- package/src/c_yui_window_manager.js +38 -4
- package/src/json_view_helpers.js +214 -0
- package/src/json_view_helpers.test.js +135 -0
- package/src/route_map_model.js +317 -0
- package/src/route_map_model.test.js +209 -0
- package/src/route_resolver.js +24 -1
- package/src/route_resolver.test.js +40 -1
- package/src/shell_modals.js +117 -39
- package/src/shell_route_map.css +225 -0
- package/src/shell_route_map.js +363 -0
- package/src/tabulator.css +67 -0
- package/src/yui_dev.js +130 -8
- package/src/yui_frontend_view.js +106 -0
- package/src/yui_icons.css +62 -0
- package/src/yui_inputs.css +8 -3
- package/src/yui_inputs.js +51 -8
- package/src/yui_tabulator_i18n.js +128 -0
- package/src/yui_theme.js +147 -0
- package/src/yui_time.js +666 -0
- package/src/yui_time.test.js +330 -0
- package/src/yui_toolbar.js +86 -43
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_route_map.css
|
|
3
|
+
*
|
|
4
|
+
* Styling for the site-map (route-tree) viewer in shell_route_map.js.
|
|
5
|
+
* A monospaced, indented tree; theme-aware via Bulma variables;
|
|
6
|
+
* and @media print rules (scoped by body.routemap-printing) so the
|
|
7
|
+
* Print button prints only the tree.
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2026, ArtGins.
|
|
10
|
+
* All Rights Reserved.
|
|
11
|
+
***********************************************************************/
|
|
12
|
+
.ROUTEMAP_BODY {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
width: 100%;
|
|
16
|
+
min-height: 0;
|
|
17
|
+
}
|
|
18
|
+
/* In the resizable window, fill the window body so the tree grows/shrinks
|
|
19
|
+
* with it (the WINDOW_BODY is is-flex-grow-1 with a definite height). */
|
|
20
|
+
.yui-window-body .ROUTEMAP_BODY {
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
.ROUTEMAP_SEARCH_ROW {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: 0.6rem;
|
|
27
|
+
}
|
|
28
|
+
.ROUTEMAP_SEARCH_CTRL {
|
|
29
|
+
max-width: 22rem;
|
|
30
|
+
flex: 1 1 auto;
|
|
31
|
+
}
|
|
32
|
+
.ROUTEMAP_COUNT {
|
|
33
|
+
display: inline-flex;
|
|
34
|
+
gap: 0.25rem;
|
|
35
|
+
white-space: nowrap;
|
|
36
|
+
flex: 0 0 auto;
|
|
37
|
+
}
|
|
38
|
+
.ROUTEMAP_COUNT_N {
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
}
|
|
41
|
+
/* Highlight a row that itself matches the filter. */
|
|
42
|
+
.ROUTEMAP_MATCH {
|
|
43
|
+
background: var(--bulma-warning-light, #fff6d9) !important;
|
|
44
|
+
}
|
|
45
|
+
[data-theme="dark"] .ROUTEMAP_MATCH {
|
|
46
|
+
background: rgba(255, 221, 87, 0.20) !important;
|
|
47
|
+
}
|
|
48
|
+
.ROUTEMAP_MATCH .ROUTEMAP_NAME,
|
|
49
|
+
.ROUTEMAP_MATCH .ROUTEMAP_ROUTE {
|
|
50
|
+
color: var(--bulma-text-strong, #363636);
|
|
51
|
+
}
|
|
52
|
+
[data-theme="dark"] .ROUTEMAP_MATCH .ROUTEMAP_NAME,
|
|
53
|
+
[data-theme="dark"] .ROUTEMAP_MATCH .ROUTEMAP_ROUTE {
|
|
54
|
+
color: #1a1a1a;
|
|
55
|
+
}
|
|
56
|
+
.ROUTEMAP_TREE {
|
|
57
|
+
font-family: var(--bulma-family-monospace, monospace);
|
|
58
|
+
font-size: 0.85rem;
|
|
59
|
+
line-height: 1.5;
|
|
60
|
+
overflow: auto;
|
|
61
|
+
min-height: 6rem;
|
|
62
|
+
/* Modal fallback: cap and scroll. In the window the rule below
|
|
63
|
+
* makes it flex-fill instead. */
|
|
64
|
+
flex: 0 1 auto;
|
|
65
|
+
max-height: 68vh;
|
|
66
|
+
border: 1px solid var(--bulma-border-weak, #dbdbdb);
|
|
67
|
+
border-radius: 0.375rem;
|
|
68
|
+
padding: 0.75rem 1rem;
|
|
69
|
+
background: var(--bulma-scheme-main, #fff);
|
|
70
|
+
}
|
|
71
|
+
/* In the window: grow to fill the remaining height (after the filter
|
|
72
|
+
* and print row) and scroll internally. */
|
|
73
|
+
.yui-window-body .ROUTEMAP_TREE {
|
|
74
|
+
flex: 1 1 auto;
|
|
75
|
+
max-height: none;
|
|
76
|
+
}
|
|
77
|
+
.ROUTEMAP_UL {
|
|
78
|
+
list-style: none;
|
|
79
|
+
margin: 0;
|
|
80
|
+
padding-left: 1.1rem;
|
|
81
|
+
border-left: 1px dotted var(--bulma-border, #b5b5b5);
|
|
82
|
+
}
|
|
83
|
+
.ROUTEMAP_ROOT {
|
|
84
|
+
padding-left: 0;
|
|
85
|
+
border-left: none;
|
|
86
|
+
}
|
|
87
|
+
.ROUTEMAP_LI {
|
|
88
|
+
margin: 0.05rem 0;
|
|
89
|
+
}
|
|
90
|
+
.ROUTEMAP_ROW {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 0.4rem;
|
|
94
|
+
padding: 0.05rem 0.25rem;
|
|
95
|
+
border-radius: 0.25rem;
|
|
96
|
+
}
|
|
97
|
+
.ROUTEMAP_ICON {
|
|
98
|
+
font-size: 0.95rem;
|
|
99
|
+
color: var(--bulma-text-weak, #7a7a7a);
|
|
100
|
+
}
|
|
101
|
+
a.ROUTEMAP_LINK {
|
|
102
|
+
text-decoration: none;
|
|
103
|
+
color: inherit;
|
|
104
|
+
}
|
|
105
|
+
a.ROUTEMAP_LINK:hover {
|
|
106
|
+
background: var(--bulma-background, #f5f5f5);
|
|
107
|
+
}
|
|
108
|
+
a.ROUTEMAP_LINK .ROUTEMAP_NAME {
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
color: var(--bulma-text, inherit);
|
|
111
|
+
}
|
|
112
|
+
.ROUTEMAP_NAME {
|
|
113
|
+
color: var(--bulma-text, inherit);
|
|
114
|
+
}
|
|
115
|
+
.ROUTEMAP_ROUTE {
|
|
116
|
+
background: transparent;
|
|
117
|
+
color: var(--bulma-link, #485fc7);
|
|
118
|
+
padding: 0;
|
|
119
|
+
font-size: 0.8rem;
|
|
120
|
+
}
|
|
121
|
+
/* Implementing GClass pill (teal) — distinct from route (blue) and
|
|
122
|
+
* event (grey). */
|
|
123
|
+
.ROUTEMAP_GCLASS {
|
|
124
|
+
font-family: var(--bulma-family-monospace, monospace);
|
|
125
|
+
font-size: 0.7rem;
|
|
126
|
+
padding: 0.02rem 0.35rem;
|
|
127
|
+
border-radius: 0.25rem;
|
|
128
|
+
background: var(--bulma-link-light, #eff1fa);
|
|
129
|
+
color: var(--bulma-link-dark, #3850b7);
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
}
|
|
132
|
+
[data-theme="dark"] .ROUTEMAP_GCLASS {
|
|
133
|
+
background: rgba(120, 150, 255, 0.16);
|
|
134
|
+
color: #aab8ff;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Action event badge — explicit colours (not Bulma's is-light tag) so it
|
|
138
|
+
* stays legible in dark mode. */
|
|
139
|
+
.ROUTEMAP_EVENT {
|
|
140
|
+
font-family: var(--bulma-family-monospace, monospace);
|
|
141
|
+
font-size: 0.72rem;
|
|
142
|
+
padding: 0.02rem 0.4rem;
|
|
143
|
+
border-radius: 0.25rem;
|
|
144
|
+
background: var(--bulma-background, #f0f0f0);
|
|
145
|
+
color: var(--bulma-text-strong, #363636);
|
|
146
|
+
border: 1px solid var(--bulma-border-weak, #dbdbdb);
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
[data-theme="dark"] .ROUTEMAP_EVENT {
|
|
150
|
+
background: rgba(255, 255, 255, 0.10);
|
|
151
|
+
color: #e6e6e6;
|
|
152
|
+
border-color: rgba(255, 255, 255, 0.20);
|
|
153
|
+
}
|
|
154
|
+
@media (prefers-color-scheme: dark) {
|
|
155
|
+
:root:not([data-theme="light"]) .ROUTEMAP_EVENT {
|
|
156
|
+
background: rgba(255, 255, 255, 0.10);
|
|
157
|
+
color: #e6e6e6;
|
|
158
|
+
border-color: rgba(255, 255, 255, 0.20);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/* "You are here" — the row of the route the user is currently on. */
|
|
162
|
+
.ROUTEMAP_CURRENT {
|
|
163
|
+
background: var(--bulma-link-light, #eff1fa) !important;
|
|
164
|
+
outline: 1px solid var(--bulma-link, #485fc7);
|
|
165
|
+
}
|
|
166
|
+
[data-theme="dark"] .ROUTEMAP_CURRENT {
|
|
167
|
+
background: rgba(120, 150, 255, 0.16) !important;
|
|
168
|
+
outline-color: rgba(120, 150, 255, 0.55);
|
|
169
|
+
}
|
|
170
|
+
.ROUTEMAP_HERE {
|
|
171
|
+
font-size: 0.68rem;
|
|
172
|
+
font-weight: 700;
|
|
173
|
+
text-transform: uppercase;
|
|
174
|
+
letter-spacing: 0.03em;
|
|
175
|
+
padding: 0.02rem 0.4rem;
|
|
176
|
+
border-radius: 0.25rem;
|
|
177
|
+
background: var(--bulma-link, #485fc7);
|
|
178
|
+
color: var(--bulma-link-invert, #fff);
|
|
179
|
+
white-space: nowrap;
|
|
180
|
+
}
|
|
181
|
+
.ROUTEMAP_STRUCT .ROUTEMAP_NAME {
|
|
182
|
+
color: var(--bulma-text-weak, #7a7a7a);
|
|
183
|
+
text-transform: uppercase;
|
|
184
|
+
letter-spacing: 0.03em;
|
|
185
|
+
font-size: 0.78rem;
|
|
186
|
+
font-weight: 700;
|
|
187
|
+
}
|
|
188
|
+
.ROUTEMAP_HINT {
|
|
189
|
+
color: var(--bulma-text-weak, #7a7a7a);
|
|
190
|
+
}
|
|
191
|
+
.ROUTEMAP_ACTIONS {
|
|
192
|
+
display: flex;
|
|
193
|
+
justify-content: flex-end;
|
|
194
|
+
margin-top: 0.75rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Print only the tree. On print the button clones the tree into
|
|
198
|
+
* `.routemap-print-area` (off-screen) and sets body.routemap-printing;
|
|
199
|
+
* everything else is hidden regardless of where the window/modal lives. */
|
|
200
|
+
.routemap-print-area {
|
|
201
|
+
display: none;
|
|
202
|
+
}
|
|
203
|
+
@media print {
|
|
204
|
+
body.routemap-printing > *:not(.routemap-print-area) {
|
|
205
|
+
display: none !important;
|
|
206
|
+
}
|
|
207
|
+
body.routemap-printing .routemap-print-area {
|
|
208
|
+
display: block !important;
|
|
209
|
+
}
|
|
210
|
+
.routemap-print-area .ROUTEMAP_TREE {
|
|
211
|
+
max-height: none;
|
|
212
|
+
border: none;
|
|
213
|
+
overflow: visible;
|
|
214
|
+
font-size: 10pt;
|
|
215
|
+
}
|
|
216
|
+
.routemap-print-area .ROUTEMAP_ROUTE,
|
|
217
|
+
.routemap-print-area .ROUTEMAP_NAME {
|
|
218
|
+
color: #000 !important;
|
|
219
|
+
}
|
|
220
|
+
.routemap-print-area .ROUTEMAP_EVENT {
|
|
221
|
+
background: transparent !important;
|
|
222
|
+
color: #000 !important;
|
|
223
|
+
border-color: #999 !important;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_route_map.js
|
|
3
|
+
*
|
|
4
|
+
* "Site map" viewer for a C_YUI_SHELL app: renders the WHOLE
|
|
5
|
+
* navigation surface — the toolbar (including the account menu),
|
|
6
|
+
* the primary menu, its live dynamic tabs, and each view's
|
|
7
|
+
* declared sub-routes (topics / info / schema / focus topics) —
|
|
8
|
+
* as a printable, clickable tree, in DECLARATION order (see
|
|
9
|
+
* ROUTING.md). Meant to double as the app's basic documentation.
|
|
10
|
+
* Hosted in a resizable C_YUI_WINDOW (a modal is the fallback when
|
|
11
|
+
* C_YUI_WINDOW is not registered).
|
|
12
|
+
*
|
|
13
|
+
* Usage (e.g. from an "Account → Site map" menu action):
|
|
14
|
+
* import {yui_shell_show_route_map} from
|
|
15
|
+
* "@yuneta/gobj-ui/src/shell_route_map.js";
|
|
16
|
+
* yui_shell_show_route_map(shell, {t});
|
|
17
|
+
*
|
|
18
|
+
* Copyright (c) 2026, ArtGins.
|
|
19
|
+
* All Rights Reserved.
|
|
20
|
+
***********************************************************************/
|
|
21
|
+
import "./shell_route_map.css";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createElement2,
|
|
25
|
+
refresh_language,
|
|
26
|
+
gobj_create_service,
|
|
27
|
+
gobj_find_service,
|
|
28
|
+
gobj_start,
|
|
29
|
+
gobj_send_event,
|
|
30
|
+
is_gobj,
|
|
31
|
+
gclass_find_by_name,
|
|
32
|
+
log_error,
|
|
33
|
+
} from "@yuneta/gobj-js";
|
|
34
|
+
|
|
35
|
+
import {yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
|
|
36
|
+
import {yui_shell_nav_map} from "./c_yui_shell.js";
|
|
37
|
+
import {attach_clear} from "./yui_inputs.js";
|
|
38
|
+
|
|
39
|
+
import i18next from "i18next";
|
|
40
|
+
|
|
41
|
+
const WIN_NAME = "shell-route-map-window";
|
|
42
|
+
|
|
43
|
+
/* Modal fallback currently open (the window flavour is a service and
|
|
44
|
+
* is found by name; the modal is not, so it is tracked here to make
|
|
45
|
+
* the second call a TOGGLE for both flavours). */
|
|
46
|
+
let __open_modal__ = null;
|
|
47
|
+
|
|
48
|
+
/* Bring the "you are here" row into view once the map is mounted. */
|
|
49
|
+
function scroll_to_current($body)
|
|
50
|
+
{
|
|
51
|
+
let $cur = $body.querySelector(".ROUTEMAP_CURRENT");
|
|
52
|
+
if($cur && typeof $cur.scrollIntoView === "function") {
|
|
53
|
+
$cur.scrollIntoView({block: "center"});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/***************************************************************
|
|
59
|
+
* Render one nav node as an <li> (with a nested <ul> for children),
|
|
60
|
+
* preserving the given order. A node with a `route` is a live hash
|
|
61
|
+
* link; a node with an `event` is an action (shown, not fired); a
|
|
62
|
+
* structural/group node is plain text.
|
|
63
|
+
***************************************************************/
|
|
64
|
+
function render_node(node)
|
|
65
|
+
{
|
|
66
|
+
let row = [];
|
|
67
|
+
if(node.icon && /^yi-[a-z0-9-]+$/.test(node.icon)) {
|
|
68
|
+
row.push(["span", {class: "icon ROUTEMAP_ICON"},
|
|
69
|
+
[["i", {class: node.icon}]]]);
|
|
70
|
+
}
|
|
71
|
+
row.push(["span", {class: "ROUTEMAP_NAME", i18n: node.label}, node.label]);
|
|
72
|
+
/* Documentation columns: route + implementing gclass + action event. */
|
|
73
|
+
if(node.route) {
|
|
74
|
+
row.push(["code", {class: "ROUTEMAP_ROUTE"}, node.route]);
|
|
75
|
+
}
|
|
76
|
+
if(node.gclass) {
|
|
77
|
+
row.push(["span", {class: "ROUTEMAP_GCLASS",
|
|
78
|
+
title: "gclass", "data-i18n-title": "gclass"},
|
|
79
|
+
node.gclass]);
|
|
80
|
+
}
|
|
81
|
+
if(node.event) {
|
|
82
|
+
row.push(["span", {class: "ROUTEMAP_EVENT"}, node.event]);
|
|
83
|
+
}
|
|
84
|
+
/* "You are here" — the node whose route best matches the current
|
|
85
|
+
* one (marked by the model; at most one). */
|
|
86
|
+
if(node.current) {
|
|
87
|
+
row.push(["span", {class: "ROUTEMAP_HERE", i18n: "you are here"},
|
|
88
|
+
"you are here"]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let row_class = node.current ? " ROUTEMAP_CURRENT" : "";
|
|
92
|
+
let $row;
|
|
93
|
+
if(node.route) {
|
|
94
|
+
$row = createElement2(
|
|
95
|
+
["a", {class: "ROUTEMAP_LINK ROUTEMAP_ROW" + row_class,
|
|
96
|
+
href: "#" + node.route,
|
|
97
|
+
title: node.route}, row]);
|
|
98
|
+
} else {
|
|
99
|
+
$row = createElement2(
|
|
100
|
+
["span", {class: "ROUTEMAP_ROW ROUTEMAP_STRUCT" + row_class}, row]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let kids = (node.children && node.children.length)
|
|
104
|
+
? [createElement2(["ul", {class: "ROUTEMAP_UL"},
|
|
105
|
+
node.children.map(render_node)])]
|
|
106
|
+
: [];
|
|
107
|
+
return createElement2(["li", {class: "ROUTEMAP_LI"}, [$row].concat(kids)]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/***************************************************************
|
|
111
|
+
* Filter a rendered <li>: visible when its own row matches `q`, when
|
|
112
|
+
* an ancestor matched (show the whole subtree of a match), or when a
|
|
113
|
+
* descendant matches (keep the ancestor path). Empty `q` shows all.
|
|
114
|
+
* Matches against the visible row text (name + route + event), so it
|
|
115
|
+
* honours the current translation. Returns whether the <li> is shown.
|
|
116
|
+
***************************************************************/
|
|
117
|
+
function filter_li($li, q, ancestor_match)
|
|
118
|
+
{
|
|
119
|
+
let $row = $li.querySelector(":scope > .ROUTEMAP_ROW");
|
|
120
|
+
let text = $row ? ($row.textContent || "").toLowerCase() : "";
|
|
121
|
+
let self_match = q === "" || text.indexOf(q) >= 0;
|
|
122
|
+
let show_all = ancestor_match || self_match;
|
|
123
|
+
|
|
124
|
+
let any_child = false;
|
|
125
|
+
let $ul = $li.querySelector(":scope > ul");
|
|
126
|
+
if($ul) {
|
|
127
|
+
let kids = $ul.children;
|
|
128
|
+
for(let i = 0; i < kids.length; i++) {
|
|
129
|
+
if(filter_li(kids[i], q, show_all)) {
|
|
130
|
+
any_child = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let visible = show_all || any_child;
|
|
136
|
+
$li.style.display = visible ? "" : "none";
|
|
137
|
+
if($row) {
|
|
138
|
+
$row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match);
|
|
139
|
+
}
|
|
140
|
+
return visible;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/***************************************************************
|
|
144
|
+
* Build the site-map body (tree + hint + print), and wire the
|
|
145
|
+
* print button and the link-jump behaviour. Links navigate
|
|
146
|
+
* natively; the shell's transient-overlay drain closes the host
|
|
147
|
+
* on a resting-route change. `on_jump()` closes the host for the
|
|
148
|
+
* one click that navigates nowhere (the current route).
|
|
149
|
+
***************************************************************/
|
|
150
|
+
function build_body(shell, t, on_jump)
|
|
151
|
+
{
|
|
152
|
+
let map = yui_shell_nav_map(shell);
|
|
153
|
+
let children = [
|
|
154
|
+
{label: "toolbar", icon: "", route: "", event: "",
|
|
155
|
+
kind: "group", children: map.toolbar}
|
|
156
|
+
].concat(map.nav);
|
|
157
|
+
/* Routes declared only in the route table (config.shell.routes)
|
|
158
|
+
* that no menu/toolbar item points at — root "/", URL-only action
|
|
159
|
+
* routes. Rendered last, as their own group. */
|
|
160
|
+
if(Array.isArray(map.other) && map.other.length) {
|
|
161
|
+
children.push({label: "other routes", icon: "", route: "", event: "",
|
|
162
|
+
kind: "group", children: map.other});
|
|
163
|
+
}
|
|
164
|
+
let root = {
|
|
165
|
+
label: map.brand.label || "app",
|
|
166
|
+
icon: "",
|
|
167
|
+
route: map.brand.route || "",
|
|
168
|
+
event: "",
|
|
169
|
+
current: !!map.brand.current,
|
|
170
|
+
children: children
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let $tree = createElement2(
|
|
174
|
+
["div", {class: "ROUTEMAP_TREE"}, [
|
|
175
|
+
createElement2(["ul", {class: "ROUTEMAP_UL ROUTEMAP_ROOT"},
|
|
176
|
+
[render_node(root)]])
|
|
177
|
+
]]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/* Search filter: matching nodes plus their ancestor path (and the
|
|
181
|
+
* matched node's whole subtree) stay visible; the rest collapse. */
|
|
182
|
+
let $search = createElement2(
|
|
183
|
+
["input", {class: "input is-small ROUTEMAP_SEARCH", type: "text",
|
|
184
|
+
placeholder: t("filter", {defaultValue: "Filter…"}),
|
|
185
|
+
"data-i18n-placeholder": "filter",
|
|
186
|
+
"aria-label": t("filter", {defaultValue: "Filter"}),
|
|
187
|
+
"data-i18n-aria-label": "filter"}]
|
|
188
|
+
);
|
|
189
|
+
let $search_ctrl = createElement2(
|
|
190
|
+
["div", {class: "control ROUTEMAP_SEARCH_CTRL"}, [$search]]
|
|
191
|
+
);
|
|
192
|
+
attach_clear($search_ctrl, $search);
|
|
193
|
+
|
|
194
|
+
/* Live match counter next to the filter. */
|
|
195
|
+
let $count_n = createElement2(["span", {class: "ROUTEMAP_COUNT_N"}, ""]);
|
|
196
|
+
let $count = createElement2(
|
|
197
|
+
["span", {class: "ROUTEMAP_COUNT is-size-7 has-text-grey is-hidden"},
|
|
198
|
+
[$count_n, ["span", {i18n: "matches"}, "matches"]]]
|
|
199
|
+
);
|
|
200
|
+
let $search_row = createElement2(
|
|
201
|
+
["div", {class: "ROUTEMAP_SEARCH_ROW mb-2"}, [$search_ctrl, $count]]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
$search.addEventListener("input", function() {
|
|
205
|
+
let q = ($search.value || "").trim().toLowerCase();
|
|
206
|
+
let $root_li = $tree.querySelector(".ROUTEMAP_ROOT > li");
|
|
207
|
+
if($root_li) {
|
|
208
|
+
filter_li($root_li, q, false);
|
|
209
|
+
}
|
|
210
|
+
if(q === "") {
|
|
211
|
+
$count.classList.add("is-hidden");
|
|
212
|
+
} else {
|
|
213
|
+
$count_n.textContent = $tree.querySelectorAll(".ROUTEMAP_MATCH").length;
|
|
214
|
+
$count.classList.remove("is-hidden");
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
let $body = createElement2(
|
|
219
|
+
["div", {class: "C_YUI_SHELL_ROUTEMAP ROUTEMAP_BODY"}, [
|
|
220
|
+
["p", {class: "ROUTEMAP_HINT is-size-7 mb-2", i18n: "site map hint"},
|
|
221
|
+
t("site map hint", {defaultValue:
|
|
222
|
+
"Every reachable position of the app is a URL. Click to jump."})],
|
|
223
|
+
$search_row,
|
|
224
|
+
$tree,
|
|
225
|
+
["div", {class: "ROUTEMAP_ACTIONS"}, [
|
|
226
|
+
["button", {class: "button is-small ROUTEMAP_PRINT",
|
|
227
|
+
i18n: "print"}, t("print", {defaultValue: "Print"})]
|
|
228
|
+
]]
|
|
229
|
+
]]
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
/* Print only the tree: clone it into an off-screen print area so
|
|
233
|
+
* the @media print rules can hide everything else, regardless of
|
|
234
|
+
* where the window/modal is mounted. */
|
|
235
|
+
let $print = $body.querySelector(".ROUTEMAP_PRINT");
|
|
236
|
+
if($print) {
|
|
237
|
+
$print.addEventListener("click", function() {
|
|
238
|
+
let $area = document.createElement("div");
|
|
239
|
+
$area.className = "routemap-print-area";
|
|
240
|
+
$area.appendChild($tree.cloneNode(true));
|
|
241
|
+
document.body.appendChild($area);
|
|
242
|
+
document.body.classList.add("routemap-printing");
|
|
243
|
+
try {
|
|
244
|
+
window.print();
|
|
245
|
+
} finally {
|
|
246
|
+
document.body.classList.remove("routemap-printing");
|
|
247
|
+
if($area.parentNode) {
|
|
248
|
+
$area.parentNode.removeChild($area);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* A route link jumps there NATIVELY (browser push + hashchange —
|
|
255
|
+
* no preventDefault): a resting-route change then closes this
|
|
256
|
+
* window/modal through the shell's transient-overlay drain, which
|
|
257
|
+
* is deterministic (the old close-then-deferred-navigate raced the
|
|
258
|
+
* dismissal's history.back() and could land back where it
|
|
259
|
+
* started). A subpath or action-route jump keeps the map open —
|
|
260
|
+
* it doubles as a navigation panel. Clicking the route the user
|
|
261
|
+
* is ALREADY on navigates nowhere: close the host instead.
|
|
262
|
+
* Action nodes (no route) are documentation only and do not fire. */
|
|
263
|
+
$body.addEventListener("click", function(ev) {
|
|
264
|
+
let $link = ev.target && ev.target.closest &&
|
|
265
|
+
ev.target.closest(".ROUTEMAP_LINK");
|
|
266
|
+
if(!$link) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
let href = $link.getAttribute("href");
|
|
270
|
+
if(href && typeof window !== "undefined" &&
|
|
271
|
+
window.location.hash === href &&
|
|
272
|
+
typeof on_jump === "function") {
|
|
273
|
+
ev.preventDefault();
|
|
274
|
+
on_jump();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
refresh_language($body, t);
|
|
279
|
+
return $body;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
/***************************************************************
|
|
284
|
+
* Show the site map. Toggles: a second call closes the open one.
|
|
285
|
+
***************************************************************/
|
|
286
|
+
export function yui_shell_show_route_map(shell, opts)
|
|
287
|
+
{
|
|
288
|
+
let t = (opts && opts.t) || i18next.t.bind(i18next);
|
|
289
|
+
|
|
290
|
+
/* Toggle: an open site-map window/modal → close it. */
|
|
291
|
+
let existing = gobj_find_service(WIN_NAME, false);
|
|
292
|
+
if(existing && is_gobj(existing)) {
|
|
293
|
+
gobj_send_event(existing, "EV_CLOSE_WINDOW", {}, shell);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if(__open_modal__) {
|
|
297
|
+
let m = __open_modal__;
|
|
298
|
+
__open_modal__ = null;
|
|
299
|
+
if(typeof m.close === "function") {
|
|
300
|
+
m.close();
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Preferred: a resizable, maximisable floating window. */
|
|
306
|
+
if(gclass_find_by_name("C_YUI_WINDOW") !== null) {
|
|
307
|
+
let win_ref = {gobj: null};
|
|
308
|
+
let $body = build_body(shell, t, function() {
|
|
309
|
+
if(win_ref.gobj && is_gobj(win_ref.gobj)) {
|
|
310
|
+
gobj_send_event(win_ref.gobj, "EV_CLOSE_WINDOW", {}, shell);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
let $parent = yui_shell_popup_layer(shell) ||
|
|
314
|
+
(typeof document !== "undefined" &&
|
|
315
|
+
document.getElementById("top-layer")) || null;
|
|
316
|
+
let win = gobj_create_service(WIN_NAME, "C_YUI_WINDOW", {
|
|
317
|
+
$parent: $parent,
|
|
318
|
+
subscriber: null,
|
|
319
|
+
modal: false,
|
|
320
|
+
showMax: true,
|
|
321
|
+
showFooter: false,
|
|
322
|
+
resizable: true,
|
|
323
|
+
center: true,
|
|
324
|
+
auto_save_size_and_position: true,
|
|
325
|
+
width: 780,
|
|
326
|
+
height: 640,
|
|
327
|
+
logical_class: "ROUTEMAP_WINDOW",
|
|
328
|
+
title: "site map",
|
|
329
|
+
icon: "yi-bars",
|
|
330
|
+
body: $body,
|
|
331
|
+
manager: null
|
|
332
|
+
}, shell);
|
|
333
|
+
if(!win) {
|
|
334
|
+
log_error("C_YUI_SHELL: cannot create the site-map window");
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
win_ref.gobj = win;
|
|
338
|
+
gobj_start(win);
|
|
339
|
+
scroll_to_current($body);
|
|
340
|
+
return win;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Fallback: a modal (no C_YUI_WINDOW registered). */
|
|
344
|
+
let modal_ref = {modal: null};
|
|
345
|
+
let $body = build_body(shell, t, function() {
|
|
346
|
+
if(modal_ref.modal && typeof modal_ref.modal.close === "function") {
|
|
347
|
+
modal_ref.modal.close();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
modal_ref.modal = yui_shell_show_modal(shell, $body, {
|
|
351
|
+
dialog: true,
|
|
352
|
+
logical_class: "ROUTEMAP_SHEET",
|
|
353
|
+
/* The KEY, not t(key): the modal renders `title` with
|
|
354
|
+
* data-i18n, so a pre-translated string would become the
|
|
355
|
+
* "key" and never re-translate. */
|
|
356
|
+
title: "site map",
|
|
357
|
+
t: t,
|
|
358
|
+
on_close: function() { __open_modal__ = null; }
|
|
359
|
+
});
|
|
360
|
+
__open_modal__ = modal_ref.modal;
|
|
361
|
+
scroll_to_current($body);
|
|
362
|
+
return modal_ref.modal;
|
|
363
|
+
}
|
package/src/tabulator.css
CHANGED
|
@@ -99,6 +99,39 @@
|
|
|
99
99
|
background-color: var(--bulma-background, #ebebeb) !important;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/* ====================================================================
|
|
103
|
+
* Opt-out of the whole-row hover wash + pointer cursor. For tables that
|
|
104
|
+
* select ONLY through the checkbox column (selectableRows:"highlight"),
|
|
105
|
+
* the hover highlight reads as a selection. Add `yui-no-row-hover` on the
|
|
106
|
+
* .tabulator element: unselected rows stay visually static on hover; a
|
|
107
|
+
* checkbox-selected row keeps its `.tabulator-selected` colour (the
|
|
108
|
+
* :not() guard leaves it alone). Colours mirror the striping rules so
|
|
109
|
+
* hover === the row's own background in every theme.
|
|
110
|
+
* ==================================================================== */
|
|
111
|
+
.tabulator.yui-no-row-hover .tabulator-row.tabulator-selectable:hover {
|
|
112
|
+
cursor: default !important;
|
|
113
|
+
}
|
|
114
|
+
.tabulator.yui-no-row-hover
|
|
115
|
+
.tabulator-row.tabulator-row-odd.tabulator-selectable:not(.tabulator-selected):hover {
|
|
116
|
+
background-color: var(--bulma-scheme-main, #fff) !important;
|
|
117
|
+
color: var(--bulma-body-color) !important;
|
|
118
|
+
}
|
|
119
|
+
.tabulator.yui-no-row-hover
|
|
120
|
+
.tabulator-row.tabulator-row-even.tabulator-selectable:not(.tabulator-selected):hover {
|
|
121
|
+
background-color: var(--bulma-scheme-main-ter, #f5f5f5) !important;
|
|
122
|
+
color: var(--bulma-body-color) !important;
|
|
123
|
+
}
|
|
124
|
+
[data-theme=dark] .tabulator.yui-no-row-hover
|
|
125
|
+
.tabulator-row.tabulator-row-odd.tabulator-selectable:not(.tabulator-selected):hover {
|
|
126
|
+
background-color: var(--bulma-body-background-color) !important;
|
|
127
|
+
color: var(--bulma-body-color) !important;
|
|
128
|
+
}
|
|
129
|
+
[data-theme=dark] .tabulator.yui-no-row-hover
|
|
130
|
+
.tabulator-row.tabulator-row-even.tabulator-selectable:not(.tabulator-selected):hover {
|
|
131
|
+
background-color: var(--bulma-background) !important;
|
|
132
|
+
color: var(--bulma-body-color) !important;
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
/*
|
|
103
136
|
* Tabulator dark theme
|
|
104
137
|
* Activated by html[data-theme=dark] (set by the app's theme switcher)
|
|
@@ -143,6 +176,30 @@
|
|
|
143
176
|
color: var(--bulma-text) !important;
|
|
144
177
|
}
|
|
145
178
|
|
|
179
|
+
/*
|
|
180
|
+
* The CELL EDITOR in dark theme.
|
|
181
|
+
*
|
|
182
|
+
* Tabulator gives its editor input no colour of its own, so it inherits the
|
|
183
|
+
* browser default: BLACK text on the dark cell — the value disappears the
|
|
184
|
+
* moment you click into it, and comes back when the field loses focus. Same
|
|
185
|
+
* for a header-filter input and for a <select>.
|
|
186
|
+
*/
|
|
187
|
+
[data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing input,
|
|
188
|
+
[data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing select,
|
|
189
|
+
[data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing textarea,
|
|
190
|
+
[data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input,
|
|
191
|
+
[data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter select {
|
|
192
|
+
background-color: var(--bulma-scheme-main, #16181d);
|
|
193
|
+
color: var(--bulma-body-color, #e6e6e6) !important;
|
|
194
|
+
caret-color: var(--bulma-body-color, #e6e6e6);
|
|
195
|
+
border-color: var(--bulma-border);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
[data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing input::placeholder,
|
|
199
|
+
[data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input::placeholder {
|
|
200
|
+
color: var(--bulma-text-weak, #8a8f98);
|
|
201
|
+
}
|
|
202
|
+
|
|
146
203
|
[data-theme=dark] .tabulator-row.tabulator-selected {
|
|
147
204
|
background-color: var(--bulma-link) !important;
|
|
148
205
|
color: var(--bulma-link-invert) !important;
|
|
@@ -209,6 +266,16 @@
|
|
|
209
266
|
[data-theme=system] .tabulator-row.tabulator-selectable:hover {
|
|
210
267
|
background-color: var(--bulma-background) !important;
|
|
211
268
|
}
|
|
269
|
+
[data-theme=system] .tabulator.yui-no-row-hover
|
|
270
|
+
.tabulator-row.tabulator-row-odd.tabulator-selectable:not(.tabulator-selected):hover {
|
|
271
|
+
background-color: var(--bulma-body-background-color) !important;
|
|
272
|
+
color: var(--bulma-body-color) !important;
|
|
273
|
+
}
|
|
274
|
+
[data-theme=system] .tabulator.yui-no-row-hover
|
|
275
|
+
.tabulator-row.tabulator-row-even.tabulator-selectable:not(.tabulator-selected):hover {
|
|
276
|
+
background-color: var(--bulma-background) !important;
|
|
277
|
+
color: var(--bulma-body-color) !important;
|
|
278
|
+
}
|
|
212
279
|
[data-theme=system] .tabulator-row.tabulator-selected {
|
|
213
280
|
background-color: var(--bulma-link) !important;
|
|
214
281
|
color: var(--bulma-link-invert) !important;
|