@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
package/src/c_yui_shell.js
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
gobj_read_attr, gobj_read_pointer_attr, gobj_write_attr,
|
|
43
43
|
createElement2, empty_string, is_object, is_array, is_string,
|
|
44
44
|
refresh_language,
|
|
45
|
+
is_gobj,
|
|
45
46
|
} from "@yuneta/gobj-js";
|
|
46
47
|
|
|
47
48
|
import {
|
|
@@ -59,7 +60,8 @@ import {
|
|
|
59
60
|
validate_toolbar_item,
|
|
60
61
|
} from "./shell_toolbar_helpers.js";
|
|
61
62
|
|
|
62
|
-
import { resolve_route } from "./route_resolver.js";
|
|
63
|
+
import { resolve_route, normalize_route } from "./route_resolver.js";
|
|
64
|
+
import { build_nav_map } from "./route_map_model.js";
|
|
63
65
|
import {
|
|
64
66
|
section_index_target,
|
|
65
67
|
secondary_nav_renders,
|
|
@@ -102,11 +104,73 @@ SDATA(data_type_t.DTP_BOOLEAN, "use_hash", 0, true, "Bind navigation to
|
|
|
102
104
|
SDATA(data_type_t.DTP_POINTER, "mount_element", 0, null, "HTMLElement to mount shell into (default: document.body)"),
|
|
103
105
|
|
|
104
106
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of the shell"),
|
|
105
|
-
SDATA(data_type_t.DTP_POINTER, "priv", 0, null, "Private runtime state (zones/layers/stages/navs)"),
|
|
106
107
|
SDATA_END()
|
|
107
108
|
];
|
|
108
109
|
|
|
109
|
-
let PRIVATE_DATA = {
|
|
110
|
+
let PRIVATE_DATA = {
|
|
111
|
+
zones: {},
|
|
112
|
+
layers: {},
|
|
113
|
+
stages: {},
|
|
114
|
+
navs: [],
|
|
115
|
+
item_index: {},
|
|
116
|
+
/* Sub-route contributor registry (ROUTING.md): a mounted view
|
|
117
|
+
* declares the deep, view-owned children of its base route
|
|
118
|
+
* (topics, /info, /schema, focus topics — subpaths that are NOT
|
|
119
|
+
* declared routes) so the site map can show the full tree. Keyed
|
|
120
|
+
* by base route → ordered [{route,label,icon,children?}]. */
|
|
121
|
+
sub_routes: {},
|
|
122
|
+
/* Event → handler GClass(es) registry (ROUTING.md): a gclass that
|
|
123
|
+
* handles a toolbar/account action event self-declares so the site
|
|
124
|
+
* map can show WHERE the action is implemented. Keyed by event name
|
|
125
|
+
* → array of gclass names. */
|
|
126
|
+
event_handlers: {},
|
|
127
|
+
hash_handler: null,
|
|
128
|
+
keydown_handler: null,
|
|
129
|
+
/* Escape priority chain: array of { layer, handler }. Each
|
|
130
|
+
* interactive overlay (drawer today, modal/popup tomorrow)
|
|
131
|
+
* pushes its close handler when it opens and pops it when
|
|
132
|
+
* it closes. Escape calls the top handler only — LIFO. */
|
|
133
|
+
escape_stack: [],
|
|
134
|
+
/* Overlay history integration (Back button ↔ modals/windows).
|
|
135
|
+
* Each history-participating overlay (modal, floating window)
|
|
136
|
+
* pushes a { id, close } record here when it opens, plus a
|
|
137
|
+
* synthetic browser-history entry (pushState). The browser Back
|
|
138
|
+
* button then closes the TOP overlay instead of navigating; and
|
|
139
|
+
* an overlay dismissed by any other path (X, Escape, backdrop,
|
|
140
|
+
* code) retires its history entry via history.back(). Gated on
|
|
141
|
+
* `use_hash` — see push_overlay_history / overlay_dismissed. */
|
|
142
|
+
overlay_stack: [],
|
|
143
|
+
overlay_seq: 0,
|
|
144
|
+
/* Count of history.back() calls WE issued to retire a dismissed
|
|
145
|
+
* overlay; the popstate they trigger is expected and ignored. */
|
|
146
|
+
expected_pops: 0,
|
|
147
|
+
popstate_handler: null,
|
|
148
|
+
/* Avatar item support — every toolbar item with type:"avatar"
|
|
149
|
+
* registers its <span> here so refresh_avatars() can repaint
|
|
150
|
+
* the initials when the host (wattyzer, hidraulia, …) calls
|
|
151
|
+
* yui_shell_set_avatar_provider() / yui_shell_refresh_avatars().
|
|
152
|
+
* The provider is a () => string callback owned by the host. */
|
|
153
|
+
avatar_provider: null,
|
|
154
|
+
avatar_nodes: [],
|
|
155
|
+
/* Optional i18n translator (t-function) the host registers via
|
|
156
|
+
* yui_shell_set_translator(). The host translates the static
|
|
157
|
+
* shell tree by calling refresh_language($container, t) once,
|
|
158
|
+
* but LAZILY-built DOM (the toolbar dropdown panel) is mounted
|
|
159
|
+
* on the popup layer, OUTSIDE $container, AFTER that call — so
|
|
160
|
+
* it would never be translated. When a translator is set the
|
|
161
|
+
* shell re-applies it to each freshly built panel. */
|
|
162
|
+
translator: null,
|
|
163
|
+
/* Connection-indicator support — every toolbar item with
|
|
164
|
+
* type:"connection" registers its dot <span> here so
|
|
165
|
+
* yui_shell_set_connection_state(shell, bool) can repaint the
|
|
166
|
+
* backend-connected state. State is host/event-driven (unlike
|
|
167
|
+
* the avatar provider it is a setter, not a pull callback). */
|
|
168
|
+
conn_nodes: [],
|
|
169
|
+
/* Currently open toolbar dropdown panel, if any. Tracked here
|
|
170
|
+
* so a second click on any trigger (or programmatic close) can
|
|
171
|
+
* tear down the previous one through the same code path. */
|
|
172
|
+
active_dropdown: null
|
|
173
|
+
};
|
|
110
174
|
|
|
111
175
|
let __gclass__ = null;
|
|
112
176
|
|
|
@@ -133,47 +197,6 @@ function mt_create(gobj)
|
|
|
133
197
|
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
134
198
|
}
|
|
135
199
|
|
|
136
|
-
/* Per-instance private state (avoid the gclass-level PRIVATE_DATA). */
|
|
137
|
-
gobj_write_attr(gobj, "priv", {
|
|
138
|
-
zones: {},
|
|
139
|
-
layers: {},
|
|
140
|
-
stages: {},
|
|
141
|
-
navs: [],
|
|
142
|
-
item_index: {},
|
|
143
|
-
hash_handler: null,
|
|
144
|
-
keydown_handler: null,
|
|
145
|
-
/* Escape priority chain: array of { layer, handler }. Each
|
|
146
|
-
* interactive overlay (drawer today, modal/popup tomorrow)
|
|
147
|
-
* pushes its close handler when it opens and pops it when
|
|
148
|
-
* it closes. Escape calls the top handler only — LIFO. */
|
|
149
|
-
escape_stack: [],
|
|
150
|
-
/* Avatar item support — every toolbar item with type:"avatar"
|
|
151
|
-
* registers its <span> here so refresh_avatars() can repaint
|
|
152
|
-
* the initials when the host (wattyzer, hidraulia, …) calls
|
|
153
|
-
* yui_shell_set_avatar_provider() / yui_shell_refresh_avatars().
|
|
154
|
-
* The provider is a () => string callback owned by the host. */
|
|
155
|
-
avatar_provider: null,
|
|
156
|
-
avatar_nodes: [],
|
|
157
|
-
/* Optional i18n translator (t-function) the host registers via
|
|
158
|
-
* yui_shell_set_translator(). The host translates the static
|
|
159
|
-
* shell tree by calling refresh_language($container, t) once,
|
|
160
|
-
* but LAZILY-built DOM (the toolbar dropdown panel) is mounted
|
|
161
|
-
* on the popup layer, OUTSIDE $container, AFTER that call — so
|
|
162
|
-
* it would never be translated. When a translator is set the
|
|
163
|
-
* shell re-applies it to each freshly built panel. */
|
|
164
|
-
translator: null,
|
|
165
|
-
/* Connection-indicator support — every toolbar item with
|
|
166
|
-
* type:"connection" registers its dot <span> here so
|
|
167
|
-
* yui_shell_set_connection_state(shell, bool) can repaint the
|
|
168
|
-
* backend-connected state. State is host/event-driven (unlike
|
|
169
|
-
* the avatar provider it is a setter, not a pull callback). */
|
|
170
|
-
conn_nodes: [],
|
|
171
|
-
/* Currently open toolbar dropdown panel, if any. Tracked here
|
|
172
|
-
* so a second click on any trigger (or programmatic close) can
|
|
173
|
-
* tear down the previous one through the same code path. */
|
|
174
|
-
active_dropdown: null
|
|
175
|
-
});
|
|
176
|
-
|
|
177
200
|
__last_shell__ = gobj;
|
|
178
201
|
|
|
179
202
|
build_ui(gobj);
|
|
@@ -185,7 +208,7 @@ function mt_create(gobj)
|
|
|
185
208
|
function mt_start(gobj)
|
|
186
209
|
{
|
|
187
210
|
let config = gobj_read_attr(gobj, "config") || {};
|
|
188
|
-
let priv =
|
|
211
|
+
let priv = gobj.priv;
|
|
189
212
|
|
|
190
213
|
/* Validate the declarative config before anything reads it. This is a
|
|
191
214
|
* system boundary (app-supplied JSON); validation makes shape errors
|
|
@@ -217,6 +240,69 @@ function mt_start(gobj)
|
|
|
217
240
|
}
|
|
218
241
|
};
|
|
219
242
|
window.addEventListener("hashchange", priv.hash_handler);
|
|
243
|
+
|
|
244
|
+
/* Back button ↔ overlays. A synthetic overlay history entry
|
|
245
|
+
* keeps the same hash, so Back over it fires popstate WITHOUT a
|
|
246
|
+
* hashchange: close the top overlay and consume the event. A
|
|
247
|
+
* real route Back changes the hash and is handled by hash_handler
|
|
248
|
+
* above. */
|
|
249
|
+
priv.popstate_handler = (ev) => {
|
|
250
|
+
let p = gobj.priv;
|
|
251
|
+
if(!p) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if(p.expected_pops > 0) {
|
|
255
|
+
/* A history.back() we issued to retire a dismissed
|
|
256
|
+
* overlay — the teardown already happened. */
|
|
257
|
+
p.expected_pops--;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if(p.overlay_stack.length === 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
let top = p.overlay_stack[p.overlay_stack.length - 1];
|
|
264
|
+
/* Did this traversal step OFF the top overlay's marker?
|
|
265
|
+
* That is the only landing that closes it, and the marker's
|
|
266
|
+
* OWN hash is what says so: pushState() with no URL keeps
|
|
267
|
+
* the current one, so stepping off a marker always lands on
|
|
268
|
+
* the marker's hash. A landing on any OTHER hash is a route
|
|
269
|
+
* traversal (fragment pushes fire popstate too) and belongs
|
|
270
|
+
* to the hashchange handler — navigate_to() closes the
|
|
271
|
+
* transient overlays when the RESTING route changes, so a
|
|
272
|
+
* subpath move keeps them open.
|
|
273
|
+
*
|
|
274
|
+
* Compare against the MARKER's hash, never against the
|
|
275
|
+
* resting route's: an action route (ROUTING.md §7.1) is
|
|
276
|
+
* transient — `current_route` stays on the underlying
|
|
277
|
+
* resting view while the URL sits on the action route — so
|
|
278
|
+
* a `stay` modal's marker legitimately lives on a hash that
|
|
279
|
+
* is NOT the resting one, and matching on the resting route
|
|
280
|
+
* left Back unable to close it. */
|
|
281
|
+
if(window.location.hash !== top.hash) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
/* Which entry did it land ON? Markers ({__yui_overlay__:
|
|
285
|
+
* id}) tag synthetic entries; route entries have a null
|
|
286
|
+
* state.
|
|
287
|
+
* - Landing ON the top overlay's OWN entry (a Back from a
|
|
288
|
+
* subpath entry pushed above it, or an odd
|
|
289
|
+
* history.go(n)): the overlay is at its home entry —
|
|
290
|
+
* keep it open.
|
|
291
|
+
* - Any other landing (the route entry below, or a lower
|
|
292
|
+
* overlay's marker) closes the top overlay: strict
|
|
293
|
+
* LIFO. */
|
|
294
|
+
let st = ev && ev.state;
|
|
295
|
+
if(st && st.__yui_overlay__ === top.id) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
p.overlay_stack.pop();
|
|
299
|
+
try {
|
|
300
|
+
top.close();
|
|
301
|
+
} catch(e) {
|
|
302
|
+
log_warning(`C_YUI_SHELL: overlay close on Back failed: ${e}`);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
window.addEventListener("popstate", priv.popstate_handler);
|
|
220
306
|
}
|
|
221
307
|
|
|
222
308
|
/* Global Escape: route to the top handler of the escape stack,
|
|
@@ -264,7 +350,7 @@ function mt_start(gobj)
|
|
|
264
350
|
***************************************************************/
|
|
265
351
|
function mt_stop(gobj)
|
|
266
352
|
{
|
|
267
|
-
let priv =
|
|
353
|
+
let priv = gobj.priv;
|
|
268
354
|
if(!priv) {
|
|
269
355
|
return;
|
|
270
356
|
}
|
|
@@ -278,6 +364,10 @@ function mt_stop(gobj)
|
|
|
278
364
|
window.removeEventListener("hashchange", priv.hash_handler);
|
|
279
365
|
priv.hash_handler = null;
|
|
280
366
|
}
|
|
367
|
+
if(priv.popstate_handler) {
|
|
368
|
+
window.removeEventListener("popstate", priv.popstate_handler);
|
|
369
|
+
priv.popstate_handler = null;
|
|
370
|
+
}
|
|
281
371
|
if(priv.keydown_handler) {
|
|
282
372
|
window.removeEventListener("keydown", priv.keydown_handler);
|
|
283
373
|
priv.keydown_handler = null;
|
|
@@ -327,7 +417,6 @@ function mt_destroy(gobj)
|
|
|
327
417
|
$container.parentNode.removeChild($container);
|
|
328
418
|
}
|
|
329
419
|
gobj_write_attr(gobj, "$container", null);
|
|
330
|
-
gobj_write_attr(gobj, "priv", null);
|
|
331
420
|
|
|
332
421
|
if(__last_shell__ === gobj) {
|
|
333
422
|
__last_shell__ = null;
|
|
@@ -514,7 +603,7 @@ function check_route_unique(route_owner, item, menu_id)
|
|
|
514
603
|
************************************************************/
|
|
515
604
|
function build_ui(gobj)
|
|
516
605
|
{
|
|
517
|
-
let priv =
|
|
606
|
+
let priv = gobj.priv;
|
|
518
607
|
let config = gobj_read_attr(gobj, "config") || {};
|
|
519
608
|
let shell_cfg = config.shell || {};
|
|
520
609
|
|
|
@@ -635,7 +724,7 @@ function apply_show_on($el, expr)
|
|
|
635
724
|
************************************************************/
|
|
636
725
|
function build_item_index(gobj, config)
|
|
637
726
|
{
|
|
638
|
-
let priv =
|
|
727
|
+
let priv = gobj.priv;
|
|
639
728
|
priv.item_index = {};
|
|
640
729
|
|
|
641
730
|
let menus = (config.menu) || {};
|
|
@@ -644,6 +733,11 @@ function build_item_index(gobj, config)
|
|
|
644
733
|
let items = (menu && menu.items) || [];
|
|
645
734
|
for(let item of items) {
|
|
646
735
|
if(item.route) {
|
|
736
|
+
/* Index by the CANONICAL form: requests are normalized
|
|
737
|
+
* (hash_to_route / navigate_to), so a key declared with
|
|
738
|
+
* a trailing or doubled slash would never match its own
|
|
739
|
+
* clicks otherwise. */
|
|
740
|
+
let item_route = normalize_route(item.route);
|
|
647
741
|
/* Section-index landing (submenu.index): the section
|
|
648
742
|
* route gets a synthesized target — the submenu itself
|
|
649
743
|
* rendered as a "cards" C_YUI_NAV in the stage — so it
|
|
@@ -659,9 +753,9 @@ function build_item_index(gobj, config)
|
|
|
659
753
|
* the common case where a `quick` drawer just reuses
|
|
660
754
|
* routes declared (with target) in `primary.submenu`.
|
|
661
755
|
* Rule: prefer the first entry with a target. */
|
|
662
|
-
let prev = priv.item_index[
|
|
756
|
+
let prev = priv.item_index[item_route];
|
|
663
757
|
if(!prev || (!prev.target && target)) {
|
|
664
|
-
priv.item_index[
|
|
758
|
+
priv.item_index[item_route] = {
|
|
665
759
|
item: item,
|
|
666
760
|
parent_item: null,
|
|
667
761
|
stage: target && target.stage || null,
|
|
@@ -674,9 +768,10 @@ function build_item_index(gobj, config)
|
|
|
674
768
|
if(sub && is_array(sub.items)) {
|
|
675
769
|
for(let sub_item of sub.items) {
|
|
676
770
|
if(sub_item.route) {
|
|
677
|
-
let
|
|
771
|
+
let sub_route = normalize_route(sub_item.route);
|
|
772
|
+
let prev = priv.item_index[sub_route];
|
|
678
773
|
if(!prev || (!prev.target && sub_item.target)) {
|
|
679
|
-
priv.item_index[
|
|
774
|
+
priv.item_index[sub_route] = {
|
|
680
775
|
item: sub_item,
|
|
681
776
|
parent_item: item,
|
|
682
777
|
stage: sub_item.target && sub_item.target.stage || null,
|
|
@@ -697,13 +792,44 @@ function build_item_index(gobj, config)
|
|
|
697
792
|
* route}; their TARGET (view gclass, or kind:"action" event +
|
|
698
793
|
* redirect) lives here so the route resolves by URL on reload /
|
|
699
794
|
* deep-link. Same precedence as menus: only fill or upgrade an
|
|
700
|
-
* entry without a target — never clobber a menu's own target.
|
|
795
|
+
* entry without a target — never clobber a menu's own target.
|
|
796
|
+
*
|
|
797
|
+
* A route IS a path, so only "/…" keys are indexed. JSON has no
|
|
798
|
+
* comments and these configs annotate themselves with sibling
|
|
799
|
+
* `_name_comment` string keys (the established idiom — see
|
|
800
|
+
* app_config.json); indexing one built a route entry whose target
|
|
801
|
+
* was the comment TEXT, which the site map then rendered as a
|
|
802
|
+
* clickable row leading nowhere, and which resolve_route would
|
|
803
|
+
* happily match. Anything else under a "/…" key is malformed and
|
|
804
|
+
* says so. */
|
|
701
805
|
let routes = (config.shell && config.shell.routes) || {};
|
|
702
806
|
for(let route in routes) {
|
|
807
|
+
if(route.charAt(0) !== "/") {
|
|
808
|
+
/* String values are the comment idiom and stay silent;
|
|
809
|
+
* an OBJECT under a slash-less key can only be a route
|
|
810
|
+
* target whose key lost its "/" — dropping it silently
|
|
811
|
+
* left the app with a menu entry that "doesn't work". */
|
|
812
|
+
if(routes[route] !== null && typeof routes[route] === "object") {
|
|
813
|
+
log_error(
|
|
814
|
+
`C_YUI_SHELL: route key '${route}' holds a route ` +
|
|
815
|
+
`target but does not start with '/' — ignored ` +
|
|
816
|
+
`(a route key is a path)`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
let route_key = normalize_route(route);
|
|
703
822
|
let t = routes[route] || null;
|
|
704
|
-
|
|
823
|
+
if(t !== null && typeof t !== "object") {
|
|
824
|
+
log_error(
|
|
825
|
+
`C_YUI_SHELL: route '${route}' target is not an object ` +
|
|
826
|
+
`(${typeof t}) — ignored`
|
|
827
|
+
);
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
let prev = priv.item_index[route_key];
|
|
705
831
|
if(!prev || (!prev.target && t)) {
|
|
706
|
-
priv.item_index[
|
|
832
|
+
priv.item_index[route_key] = {
|
|
707
833
|
item: (prev && prev.item) || null,
|
|
708
834
|
parent_item: (prev && prev.parent_item) || null,
|
|
709
835
|
stage: (t && t.stage) || null,
|
|
@@ -720,7 +846,7 @@ function build_item_index(gobj, config)
|
|
|
720
846
|
************************************************************/
|
|
721
847
|
function instantiate_menus(gobj, config)
|
|
722
848
|
{
|
|
723
|
-
let priv =
|
|
849
|
+
let priv = gobj.priv;
|
|
724
850
|
let zones_cfg = (config.shell && config.shell.zones) || {};
|
|
725
851
|
let menus = config.menu || {};
|
|
726
852
|
|
|
@@ -835,7 +961,7 @@ function render_to_obj(layout)
|
|
|
835
961
|
|
|
836
962
|
function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
|
|
837
963
|
{
|
|
838
|
-
let priv =
|
|
964
|
+
let priv = gobj.priv;
|
|
839
965
|
let render_cfg = (menu.render && (menu.render[zone_id] || menu.render["*"])) ||
|
|
840
966
|
{ layout: "vertical" };
|
|
841
967
|
if(is_string(render_cfg)) {
|
|
@@ -910,11 +1036,9 @@ function hash_to_route(hash)
|
|
|
910
1036
|
if(!hash) {
|
|
911
1037
|
return "";
|
|
912
1038
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
return s;
|
|
1039
|
+
/* Hashes come from the outside world (typed URLs, shared links):
|
|
1040
|
+
* normalize so "#/a/b/" hits the same index entry as "#/a/b". */
|
|
1041
|
+
return normalize_route(String(hash).replace(/^#/, ""));
|
|
918
1042
|
}
|
|
919
1043
|
|
|
920
1044
|
function route_to_hash(route)
|
|
@@ -927,11 +1051,38 @@ function route_to_hash(route)
|
|
|
927
1051
|
}
|
|
928
1052
|
|
|
929
1053
|
/************************************************************
|
|
930
|
-
* Navigate: make `route` active
|
|
1054
|
+
* Navigate: make `route` active. Both trailing args are internal;
|
|
1055
|
+
* external callers omit them.
|
|
1056
|
+
* depth — the redirect-recursion counter (submenu default,
|
|
1057
|
+
* unknown-route default, action redirects).
|
|
1058
|
+
* no_drain — this hop continues an action route whose event has
|
|
1059
|
+
* ALREADY fired, so an overlay on the stack may be the
|
|
1060
|
+
* one that event just opened, meant to float above the
|
|
1061
|
+
* route we are moving to. Draining would kill it.
|
|
931
1062
|
************************************************************/
|
|
932
|
-
|
|
1063
|
+
const MAX_REDIRECT_DEPTH = 8;
|
|
1064
|
+
|
|
1065
|
+
function navigate_to(gobj, route, depth, no_drain)
|
|
933
1066
|
{
|
|
934
|
-
let priv =
|
|
1067
|
+
let priv = gobj.priv;
|
|
1068
|
+
depth = depth || 0;
|
|
1069
|
+
route = normalize_route(route);
|
|
1070
|
+
|
|
1071
|
+
/* A redirect cycle (submenu default → unknown → default → …) is a
|
|
1072
|
+
* config error; without a cap it recurses to a stack overflow that
|
|
1073
|
+
* kills the app with a mute RangeError. Fail loudly instead. */
|
|
1074
|
+
if(depth > MAX_REDIRECT_DEPTH) {
|
|
1075
|
+
log_error(
|
|
1076
|
+
`C_YUI_SHELL: redirect loop navigating to '${route}' ` +
|
|
1077
|
+
`(depth > ${MAX_REDIRECT_DEPTH}) — check submenu defaults, ` +
|
|
1078
|
+
`default_route and action redirects for a cycle`
|
|
1079
|
+
);
|
|
1080
|
+
show_stage_placeholder(
|
|
1081
|
+
gobj, "main",
|
|
1082
|
+
`C_YUI_SHELL: redirect loop at route '${route}'`
|
|
1083
|
+
);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
935
1086
|
|
|
936
1087
|
/* Audit witness: publish the navigation intent FIRST, before any
|
|
937
1088
|
* validation or DOM work. This guarantees that the FSM trace and
|
|
@@ -958,7 +1109,7 @@ function navigate_to(gobj, route)
|
|
|
958
1109
|
let first_routable = sub.items && sub.items.find(it => it && it.route);
|
|
959
1110
|
let default_sub = sub.default || (first_routable && first_routable.route);
|
|
960
1111
|
if(default_sub) {
|
|
961
|
-
return navigate_to(gobj, default_sub);
|
|
1112
|
+
return navigate_to(gobj, default_sub, depth + 1);
|
|
962
1113
|
}
|
|
963
1114
|
}
|
|
964
1115
|
|
|
@@ -988,7 +1139,7 @@ function navigate_to(gobj, route)
|
|
|
988
1139
|
`C_YUI_SHELL: unknown route '${route}', ` +
|
|
989
1140
|
`redirecting to default '${def}'`
|
|
990
1141
|
);
|
|
991
|
-
return navigate_to(gobj, def);
|
|
1142
|
+
return navigate_to(gobj, def, depth + 1);
|
|
992
1143
|
}
|
|
993
1144
|
log_error(`C_YUI_SHELL: no target for route '${route}'`);
|
|
994
1145
|
show_stage_placeholder(
|
|
@@ -1022,12 +1173,32 @@ function navigate_to(gobj, route)
|
|
|
1022
1173
|
* later close → back lands on the default instead
|
|
1023
1174
|
* of exiting the app.
|
|
1024
1175
|
* EV_ROUTE_REQUESTED was already published above, so an auditor
|
|
1025
|
-
* sees the action route intent too.
|
|
1176
|
+
* sees the action route intent too.
|
|
1177
|
+
*
|
|
1178
|
+
* ORDER of event vs URL work: whenever URL work happens, it happens
|
|
1179
|
+
* BEFORE the event — the overlay a handler opens must register its
|
|
1180
|
+
* synthetic history entry over the hash it will live on:
|
|
1181
|
+
* "stay" → no URL work on a click (the URL stays where
|
|
1182
|
+
* the click pushed it; the marker lands on this
|
|
1183
|
+
* same hash). The DEEP-LINK fix-up does do URL
|
|
1184
|
+
* work (mount underneath + re-push), so there
|
|
1185
|
+
* it runs first and the event fires after.
|
|
1186
|
+
* "back"/"none" → restore the URL FIRST, event after. The old
|
|
1187
|
+
* event-first order left the overlay marker on
|
|
1188
|
+
* the ACTION hash and the restore rewrote it,
|
|
1189
|
+
* stranding the action's own route entry below:
|
|
1190
|
+
* closing the overlay then history.back()ed
|
|
1191
|
+
* onto that entry and RE-FIRED the action — the
|
|
1192
|
+
* "site-map window won't close" loop.
|
|
1193
|
+
* "<route>" → event first (logout-style: the handler may
|
|
1194
|
+
* tear the shell down; navigate afterwards). */
|
|
1026
1195
|
if(entry.target.kind === "action") {
|
|
1027
1196
|
let t = entry.target;
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1197
|
+
let publish_action = () => {
|
|
1198
|
+
if(!empty_string(t.event)) {
|
|
1199
|
+
gobj_publish_event(gobj, t.event, t.kw || {});
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1031
1202
|
let config = gobj_read_attr(gobj, "config") || {};
|
|
1032
1203
|
let prev = (priv.stages && priv.stages.main &&
|
|
1033
1204
|
priv.stages.main.active_route) ||
|
|
@@ -1042,10 +1213,23 @@ function navigate_to(gobj, route)
|
|
|
1042
1213
|
priv.stages.main.active_route);
|
|
1043
1214
|
if(!has_resting && prev && prev !== route) {
|
|
1044
1215
|
/* Deep-link / reload straight onto the overlay
|
|
1045
|
-
* route: bring up the default view underneath
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
|
|
1216
|
+
* route: bring up the default view underneath
|
|
1217
|
+
* (rewriting this initial entry), push this hash
|
|
1218
|
+
* back on top, and only THEN fire the event — the
|
|
1219
|
+
* same restore-then-event rule as `back`/`none`,
|
|
1220
|
+
* for the same reason: the overlay the handler
|
|
1221
|
+
* opens must register its synthetic entry ABOVE
|
|
1222
|
+
* the action hash, giving the history the exact
|
|
1223
|
+
* shape of the click path ([resting, action,
|
|
1224
|
+
* marker]) so ONE Back closes it. Event-first
|
|
1225
|
+
* here buried the marker (rewritten to the resting
|
|
1226
|
+
* hash) UNDER the re-pushed action hash: Back #1
|
|
1227
|
+
* landed ON the marker — own-entry check, overlay
|
|
1228
|
+
* kept open — while the URL flipped underneath,
|
|
1229
|
+
* and Back #2 re-fired the action off the initial
|
|
1230
|
+
* entry. no_drain: internal fix-up hop, not a
|
|
1231
|
+
* resting navigation. */
|
|
1232
|
+
navigate_to(gobj, prev, depth + 1, true);
|
|
1049
1233
|
let h = route_to_hash(route);
|
|
1050
1234
|
try {
|
|
1051
1235
|
window.history.pushState(null, "", h);
|
|
@@ -1054,31 +1238,52 @@ function navigate_to(gobj, route)
|
|
|
1054
1238
|
}
|
|
1055
1239
|
}
|
|
1056
1240
|
/* else: reached via a click that already pushed this
|
|
1057
|
-
* hash —
|
|
1241
|
+
* hash — the URL stays where the user sees it, and
|
|
1242
|
+
* nothing was restored, so there is no ordering to
|
|
1243
|
+
* keep: the event just fires. */
|
|
1058
1244
|
}
|
|
1245
|
+
publish_action();
|
|
1059
1246
|
return;
|
|
1060
1247
|
}
|
|
1061
1248
|
if(empty_string(redirect) || redirect === "none") {
|
|
1062
1249
|
if(gobj_read_attr(gobj, "use_hash")) {
|
|
1063
1250
|
let h = route_to_hash(prev);
|
|
1064
1251
|
try {
|
|
1065
|
-
|
|
1252
|
+
/* Keep the current entry's state: if it is an
|
|
1253
|
+
* overlay's synthetic entry, nulling the marker
|
|
1254
|
+
* would break its Back/dismiss bookkeeping. */
|
|
1255
|
+
window.history.replaceState(window.history.state, "", h);
|
|
1066
1256
|
} catch(e) {
|
|
1067
1257
|
window.location.hash = h;
|
|
1068
1258
|
}
|
|
1069
1259
|
}
|
|
1260
|
+
publish_action();
|
|
1070
1261
|
return;
|
|
1071
1262
|
}
|
|
1072
1263
|
if(redirect === "back") {
|
|
1073
|
-
|
|
1264
|
+
/* Full restore of the previous resting view + its URL (the
|
|
1265
|
+
* replaceState inside rewrites the action's pushed entry),
|
|
1266
|
+
* THEN the event: an overlay opened by the handler floats
|
|
1267
|
+
* above the restored view and its synthetic entry shares
|
|
1268
|
+
* the restored hash — Back/dismiss stay invisible. */
|
|
1269
|
+
if(prev !== route) {
|
|
1270
|
+
navigate_to(gobj, prev, depth + 1);
|
|
1271
|
+
}
|
|
1272
|
+
publish_action();
|
|
1273
|
+
return;
|
|
1074
1274
|
}
|
|
1275
|
+
publish_action();
|
|
1075
1276
|
if(redirect === route) {
|
|
1076
1277
|
log_error(
|
|
1077
1278
|
`C_YUI_SHELL: action route '${route}' redirects to itself`
|
|
1078
1279
|
);
|
|
1079
1280
|
return;
|
|
1080
1281
|
}
|
|
1081
|
-
|
|
1282
|
+
/* Event-first (logout-style: the handler may tear the shell down),
|
|
1283
|
+
* so anything the handler just put on the overlay stack belongs to
|
|
1284
|
+
* the route we are about to mount, not to the one we are leaving —
|
|
1285
|
+
* no_drain. */
|
|
1286
|
+
return navigate_to(gobj, redirect, depth + 1, true);
|
|
1082
1287
|
}
|
|
1083
1288
|
|
|
1084
1289
|
/* A fresh navigation clears any placeholder shown earlier. */
|
|
@@ -1094,6 +1299,26 @@ function navigate_to(gobj, route)
|
|
|
1094
1299
|
/* View instances are keyed by the BASE (declared) route so a
|
|
1095
1300
|
* subpath-only change reuses the same view (no rebuild). */
|
|
1096
1301
|
let prev_route = stage.active_route;
|
|
1302
|
+
|
|
1303
|
+
/* A change of RESTING route closes every Back-dismissable overlay:
|
|
1304
|
+
* they are transient (§3) and do not outlive the view they float
|
|
1305
|
+
* above. (An action route or a subpath-only move never reaches
|
|
1306
|
+
* here with a different matched_route, so they keep them open.)
|
|
1307
|
+
* Their synthetic entries are NOT retired — they are buried under
|
|
1308
|
+
* the new route's entry; popped BEFORE close, so each close's
|
|
1309
|
+
* overlay_dismissed finds its entry gone and leaves it inert for a
|
|
1310
|
+
* later Back to absorb.
|
|
1311
|
+
*
|
|
1312
|
+
* This holds at EVERY redirect depth — a submenu default, an
|
|
1313
|
+
* unknown-route default and an action's `"<route>"` redirect all
|
|
1314
|
+
* land the user on a different resting view, and gating the drain
|
|
1315
|
+
* on depth 0 made the very same click drain or not depending on
|
|
1316
|
+
* whether the target redirected. `no_drain` is the one exception,
|
|
1317
|
+
* and it is about ORDER, not depth: see the `"<route>"` hop. */
|
|
1318
|
+
if(!no_drain && prev_route && prev_route !== matched_route) {
|
|
1319
|
+
drain_overlays(gobj);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1097
1322
|
if(prev_route && prev_route !== matched_route) {
|
|
1098
1323
|
let prev_gobj = stage.items[prev_route];
|
|
1099
1324
|
let $prev = null;
|
|
@@ -1157,9 +1382,18 @@ function navigate_to(gobj, route)
|
|
|
1157
1382
|
if(gobj_read_attr(gobj, "use_hash")) {
|
|
1158
1383
|
let target_hash = route_to_hash(route);
|
|
1159
1384
|
if(window.location.hash !== target_hash) {
|
|
1160
|
-
/* Using history.replaceState avoids extra hashchange fire.
|
|
1385
|
+
/* Using history.replaceState avoids extra hashchange fire.
|
|
1386
|
+
* The current entry's state is preserved: if it is an
|
|
1387
|
+
* overlay's synthetic entry, nulling the marker would break
|
|
1388
|
+
* its Back/dismiss bookkeeping. */
|
|
1161
1389
|
try {
|
|
1162
|
-
window.history.replaceState(
|
|
1390
|
+
window.history.replaceState(window.history.state, "", target_hash);
|
|
1391
|
+
/* This rewrote the hash of the CURRENT entry. When that
|
|
1392
|
+
* entry is a live marker (a replace-navigation issued
|
|
1393
|
+
* with an overlay open), its recorded hash just went
|
|
1394
|
+
* stale — and a stale hash makes the popstate handler
|
|
1395
|
+
* miss the Back that steps off it. */
|
|
1396
|
+
retag_overlay_hash(gobj, window.history.state, target_hash);
|
|
1163
1397
|
} catch(e) {
|
|
1164
1398
|
window.location.hash = target_hash;
|
|
1165
1399
|
}
|
|
@@ -1225,7 +1459,7 @@ function build_view_gobj(gobj, entry, route, stage)
|
|
|
1225
1459
|
* (synthesized "cards" C_YUI_NAV) is SHELL-owned DOM built after
|
|
1226
1460
|
* the host's one-shot refresh_language — apply the registered
|
|
1227
1461
|
* translator, same policy as lazily-built dropdown panels. */
|
|
1228
|
-
let priv =
|
|
1462
|
+
let priv = gobj.priv;
|
|
1229
1463
|
if(priv && typeof priv.translator === "function" &&
|
|
1230
1464
|
target.gclass === "C_YUI_NAV") {
|
|
1231
1465
|
refresh_language($view, priv.translator);
|
|
@@ -1265,7 +1499,7 @@ function build_toolbar(gobj, config)
|
|
|
1265
1499
|
return;
|
|
1266
1500
|
}
|
|
1267
1501
|
|
|
1268
|
-
let priv =
|
|
1502
|
+
let priv = gobj.priv;
|
|
1269
1503
|
let zone_id = tb.zone || find_toolbar_zone(config);
|
|
1270
1504
|
let $zone = priv.zones[zone_id];
|
|
1271
1505
|
if(!$zone) {
|
|
@@ -1440,7 +1674,7 @@ function build_toolbar_brand_item(gobj, it)
|
|
|
1440
1674
|
************************************************************/
|
|
1441
1675
|
function build_toolbar_avatar_item(gobj, it)
|
|
1442
1676
|
{
|
|
1443
|
-
let priv =
|
|
1677
|
+
let priv = gobj.priv;
|
|
1444
1678
|
let aria_key = it.aria_label || it.name || it.id || "User menu";
|
|
1445
1679
|
let i18n_aria = it.aria_label || it.name || "User menu";
|
|
1446
1680
|
let action_type = (it.action && it.action.type) || "";
|
|
@@ -1495,7 +1729,7 @@ function paint_avatar(priv, $node)
|
|
|
1495
1729
|
|
|
1496
1730
|
function refresh_avatars(gobj)
|
|
1497
1731
|
{
|
|
1498
|
-
let priv =
|
|
1732
|
+
let priv = gobj.priv;
|
|
1499
1733
|
if(!priv || !is_array(priv.avatar_nodes)) {
|
|
1500
1734
|
return;
|
|
1501
1735
|
}
|
|
@@ -1531,7 +1765,7 @@ function attach_context_action(gobj, $item, it)
|
|
|
1531
1765
|
************************************************************/
|
|
1532
1766
|
function build_toolbar_connection_item(gobj, it)
|
|
1533
1767
|
{
|
|
1534
|
-
let priv =
|
|
1768
|
+
let priv = gobj.priv;
|
|
1535
1769
|
let aria_key = it.aria_label || it.name || it.id || "backend connection";
|
|
1536
1770
|
let i18n_aria = it.aria_label || it.name || "backend connection";
|
|
1537
1771
|
let attrs = {
|
|
@@ -1572,13 +1806,40 @@ function find_toolbar_zone(config)
|
|
|
1572
1806
|
return "top";
|
|
1573
1807
|
}
|
|
1574
1808
|
|
|
1809
|
+
/************************************************************
|
|
1810
|
+
* True when a click on `route` resolves to an action route whose
|
|
1811
|
+
* flavour RESTORES the previous resting URL by rewriting the
|
|
1812
|
+
* current history entry ("back", "none"/""). Pushing the action
|
|
1813
|
+
* hash first would leave that rewritten entry as an exact
|
|
1814
|
+
* duplicate of the resting entry below it — one dead Back press
|
|
1815
|
+
* per click. The click entry points navigate those directly
|
|
1816
|
+
* instead of going through the hash. ("stay" and "<route>" keep
|
|
1817
|
+
* the push: their URL genuinely moves.)
|
|
1818
|
+
************************************************************/
|
|
1819
|
+
function route_restores_url(gobj, route)
|
|
1820
|
+
{
|
|
1821
|
+
let priv = gobj.priv;
|
|
1822
|
+
if(!priv || !priv.item_index) {
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
let r = resolve_route(priv.item_index, normalize_route(route));
|
|
1826
|
+
let target = r && r.entry && r.entry.target;
|
|
1827
|
+
if(!target || target.kind !== "action") {
|
|
1828
|
+
return false;
|
|
1829
|
+
}
|
|
1830
|
+
return target.redirect === "back" ||
|
|
1831
|
+
target.redirect === "none" ||
|
|
1832
|
+
empty_string(target.redirect);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1575
1835
|
function handle_toolbar_action(gobj, item, $trigger)
|
|
1576
1836
|
{
|
|
1577
1837
|
let action = (item && item.action) || {};
|
|
1578
1838
|
switch(action.type) {
|
|
1579
1839
|
case "navigate":
|
|
1580
1840
|
if(!empty_string(action.route)) {
|
|
1581
|
-
if(gobj_read_attr(gobj, "use_hash")
|
|
1841
|
+
if(gobj_read_attr(gobj, "use_hash") &&
|
|
1842
|
+
!route_restores_url(gobj, action.route)) {
|
|
1582
1843
|
let h = route_to_hash(action.route);
|
|
1583
1844
|
if(window.location.hash !== h) {
|
|
1584
1845
|
window.location.hash = h; /* fires hashchange */
|
|
@@ -1636,7 +1897,7 @@ function handle_toolbar_action(gobj, item, $trigger)
|
|
|
1636
1897
|
************************************************************/
|
|
1637
1898
|
function toggle_toolbar_dropdown(gobj, item, action, $trigger)
|
|
1638
1899
|
{
|
|
1639
|
-
let priv =
|
|
1900
|
+
let priv = gobj.priv;
|
|
1640
1901
|
if(!priv) {
|
|
1641
1902
|
return;
|
|
1642
1903
|
}
|
|
@@ -1651,7 +1912,7 @@ function toggle_toolbar_dropdown(gobj, item, action, $trigger)
|
|
|
1651
1912
|
|
|
1652
1913
|
function open_toolbar_dropdown(gobj, item, action, $trigger)
|
|
1653
1914
|
{
|
|
1654
|
-
let priv =
|
|
1915
|
+
let priv = gobj.priv;
|
|
1655
1916
|
if(!priv || !priv.layers || !priv.layers.popup) {
|
|
1656
1917
|
return;
|
|
1657
1918
|
}
|
|
@@ -1751,7 +2012,7 @@ function open_toolbar_dropdown(gobj, item, action, $trigger)
|
|
|
1751
2012
|
|
|
1752
2013
|
function close_toolbar_dropdown(gobj)
|
|
1753
2014
|
{
|
|
1754
|
-
let priv =
|
|
2015
|
+
let priv = gobj.priv;
|
|
1755
2016
|
if(!priv) {
|
|
1756
2017
|
return;
|
|
1757
2018
|
}
|
|
@@ -1846,7 +2107,7 @@ function build_dropdown_row(gobj, sub, idx)
|
|
|
1846
2107
|
************************************************************/
|
|
1847
2108
|
function preinstantiate_eager_views(gobj)
|
|
1848
2109
|
{
|
|
1849
|
-
let priv =
|
|
2110
|
+
let priv = gobj.priv;
|
|
1850
2111
|
for(let route in priv.item_index) {
|
|
1851
2112
|
let entry = priv.item_index[route];
|
|
1852
2113
|
let t = entry.target;
|
|
@@ -1881,7 +2142,7 @@ function preinstantiate_eager_views(gobj)
|
|
|
1881
2142
|
************************************************************/
|
|
1882
2143
|
function drawers(gobj, menu_id)
|
|
1883
2144
|
{
|
|
1884
|
-
let priv =
|
|
2145
|
+
let priv = gobj.priv;
|
|
1885
2146
|
let out = [];
|
|
1886
2147
|
for(let nav of priv.navs) {
|
|
1887
2148
|
if(gobj_read_attr(nav, "layout") !== "drawer") {
|
|
@@ -1907,7 +2168,7 @@ function drawers(gobj, menu_id)
|
|
|
1907
2168
|
************************************************************/
|
|
1908
2169
|
function push_escape(gobj, layer, handler)
|
|
1909
2170
|
{
|
|
1910
|
-
let priv =
|
|
2171
|
+
let priv = gobj.priv;
|
|
1911
2172
|
if(!priv || !priv.escape_stack) {
|
|
1912
2173
|
return;
|
|
1913
2174
|
}
|
|
@@ -1916,7 +2177,7 @@ function push_escape(gobj, layer, handler)
|
|
|
1916
2177
|
|
|
1917
2178
|
function pop_escape(gobj, handler)
|
|
1918
2179
|
{
|
|
1919
|
-
let priv =
|
|
2180
|
+
let priv = gobj.priv;
|
|
1920
2181
|
if(!priv || !priv.escape_stack) {
|
|
1921
2182
|
return;
|
|
1922
2183
|
}
|
|
@@ -1926,6 +2187,139 @@ function pop_escape(gobj, handler)
|
|
|
1926
2187
|
}
|
|
1927
2188
|
}
|
|
1928
2189
|
|
|
2190
|
+
/************************************************************
|
|
2191
|
+
* Overlay history integration — Back button ↔ modals/windows.
|
|
2192
|
+
*
|
|
2193
|
+
* An overlay that wants the browser Back button to close it (modal,
|
|
2194
|
+
* floating window) registers on open with push_overlay_history and
|
|
2195
|
+
* calls overlay_dismissed when it closes by ANY other path.
|
|
2196
|
+
*
|
|
2197
|
+
* On open we push a synthetic history entry that keeps the current
|
|
2198
|
+
* hash, so routing is untouched. The two close paths converge on the
|
|
2199
|
+
* same overlay_stack, and membership in it disambiguates which one ran:
|
|
2200
|
+
*
|
|
2201
|
+
* - Back pressed → popstate pops the entry and calls entry.close();
|
|
2202
|
+
* the browser already dropped the history entry, so the later
|
|
2203
|
+
* overlay_dismissed finds the entry gone and does nothing.
|
|
2204
|
+
* - X/Escape/code → the overlay's close runs first (entry still on
|
|
2205
|
+
* the stack), overlay_dismissed removes it and history.back()s to
|
|
2206
|
+
* retire the still-present browser entry (that popstate is counted
|
|
2207
|
+
* in expected_pops and ignored).
|
|
2208
|
+
*
|
|
2209
|
+
* The synthetic entry carries a state marker ({__yui_overlay__: id})
|
|
2210
|
+
* recording which overlay it belongs to. Route navigation interplay:
|
|
2211
|
+
* - fragment navigations fire popstate too (all engines), so the
|
|
2212
|
+
* popstate handler treats only SAME-HASH landings as overlay pops;
|
|
2213
|
+
* a hash-changing landing is a route move owned by hashchange.
|
|
2214
|
+
* - navigate_to() drains (closes) every registered overlay when the
|
|
2215
|
+
* RESTING route changes — overlays are transient and do not
|
|
2216
|
+
* outlive the view they float above. An action route or a
|
|
2217
|
+
* subpath-only move keeps them open.
|
|
2218
|
+
* - a drained/buried entry is left INERT: overlay_dismissed only
|
|
2219
|
+
* history.back()s when the marker is the CURRENT history entry
|
|
2220
|
+
* (adjacent, so the back() is invisible) — never over real route
|
|
2221
|
+
* entries (that teleported the user). A later Back absorbs an
|
|
2222
|
+
* inert entry as a same-hash no-op.
|
|
2223
|
+
*
|
|
2224
|
+
* Gated on `use_hash`: an app that manages its own routing gets no
|
|
2225
|
+
* synthetic entries — a stray history.back() there could exit it.
|
|
2226
|
+
************************************************************/
|
|
2227
|
+
/* Close every registered overlay because the resting route changed.
|
|
2228
|
+
* Pop-then-close (the popstate handler's pattern): the overlay's own
|
|
2229
|
+
* close path then finds its entry gone in overlay_dismissed and does
|
|
2230
|
+
* NOT history.back() — the buried synthetic entries stay inert. */
|
|
2231
|
+
function drain_overlays(gobj)
|
|
2232
|
+
{
|
|
2233
|
+
let priv = gobj.priv;
|
|
2234
|
+
if(!priv || !priv.overlay_stack) {
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
while(priv.overlay_stack.length > 0) {
|
|
2238
|
+
let entry = priv.overlay_stack.pop();
|
|
2239
|
+
try {
|
|
2240
|
+
entry.close();
|
|
2241
|
+
} catch(e) {
|
|
2242
|
+
log_warning(`C_YUI_SHELL: overlay close on navigation failed: ${e}`);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
/* A URL rewrite moved the entry `st` tags to `hash`. When `st` is a live
|
|
2248
|
+
* overlay marker, follow it: its entry's recorded hash is the popstate
|
|
2249
|
+
* handler's authority and must not drift from the entry it describes. */
|
|
2250
|
+
function retag_overlay_hash(gobj, st, hash)
|
|
2251
|
+
{
|
|
2252
|
+
let priv = gobj.priv;
|
|
2253
|
+
if(!priv || !priv.overlay_stack || !st || !st.__yui_overlay__) {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
let entry = priv.overlay_stack.find(e => e.id === st.__yui_overlay__);
|
|
2257
|
+
if(entry) {
|
|
2258
|
+
entry.hash = hash;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
function push_overlay_history(gobj, close)
|
|
2263
|
+
{
|
|
2264
|
+
let priv = gobj.priv;
|
|
2265
|
+
if(!priv || !priv.overlay_stack || !gobj_read_attr(gobj, "use_hash")) {
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
/* `hash` = the hash the marker entry lives on (pushState with no URL
|
|
2269
|
+
* keeps the current one). The popstate handler needs it to tell a
|
|
2270
|
+
* step OFF this marker from a route traversal; it is NOT the resting
|
|
2271
|
+
* route (a `stay` action route parks the URL off it). Kept in sync
|
|
2272
|
+
* by the silent replaceState fix-up in navigate_to(). */
|
|
2273
|
+
let entry = { id: ++priv.overlay_seq, close: close,
|
|
2274
|
+
hash: window.location.hash };
|
|
2275
|
+
priv.overlay_stack.push(entry);
|
|
2276
|
+
try {
|
|
2277
|
+
window.history.pushState({ __yui_overlay__: entry.id }, "");
|
|
2278
|
+
} catch(e) {
|
|
2279
|
+
/* pushState failed (rare): drop the entry so overlay_dismissed
|
|
2280
|
+
* won't later history.back() past a real route. */
|
|
2281
|
+
priv.overlay_stack.pop();
|
|
2282
|
+
log_warning(`C_YUI_SHELL: overlay pushState failed: ${e}`);
|
|
2283
|
+
return null;
|
|
2284
|
+
}
|
|
2285
|
+
return entry;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
function overlay_dismissed(gobj, entry)
|
|
2289
|
+
{
|
|
2290
|
+
let priv = gobj.priv;
|
|
2291
|
+
if(!priv || !priv.overlay_stack || !entry) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
let idx = priv.overlay_stack.indexOf(entry);
|
|
2295
|
+
if(idx < 0) {
|
|
2296
|
+
/* Already removed by the popstate handler (closed via Back). */
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
priv.overlay_stack.splice(idx, 1);
|
|
2300
|
+
/* Retire the synthetic browser entry ONLY when it is the CURRENT
|
|
2301
|
+
* one — its state marker is live — so history.back() steps off it
|
|
2302
|
+
* invisibly (same hash, no route change). In every other case the
|
|
2303
|
+
* entry is NOT adjacent and a back() would traverse REAL entries:
|
|
2304
|
+
* - the user route-navigated after opening the overlay (the
|
|
2305
|
+
* marker sits buried under newer route entries) — a back()
|
|
2306
|
+
* here would teleport them to the pre-overlay route;
|
|
2307
|
+
* - a non-LIFO dismissal (a lower overlay closed under a higher
|
|
2308
|
+
* one) — a back() would pop past the higher overlay's entry.
|
|
2309
|
+
* Leave the entry inert instead: a later Back lands on it with the
|
|
2310
|
+
* same hash and is absorbed harmlessly. */
|
|
2311
|
+
let st = window.history.state;
|
|
2312
|
+
if(st && st.__yui_overlay__ === entry.id) {
|
|
2313
|
+
priv.expected_pops++;
|
|
2314
|
+
try {
|
|
2315
|
+
window.history.back();
|
|
2316
|
+
} catch(e) {
|
|
2317
|
+
priv.expected_pops--;
|
|
2318
|
+
log_warning(`C_YUI_SHELL: overlay history.back() failed: ${e}`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
1929
2323
|
/* Per-drawer open/close. The escape-stack entry and the focus-
|
|
1930
2324
|
* trap release function are parked on the $drawer DOM element so
|
|
1931
2325
|
* any close path (Escape, backdrop click, toolbar action, public
|
|
@@ -1989,7 +2383,7 @@ function toggle_drawer(gobj, menu_id)
|
|
|
1989
2383
|
|
|
1990
2384
|
function close_all_drawers(gobj)
|
|
1991
2385
|
{
|
|
1992
|
-
let priv =
|
|
2386
|
+
let priv = gobj.priv;
|
|
1993
2387
|
if(!priv) {
|
|
1994
2388
|
return;
|
|
1995
2389
|
}
|
|
@@ -2011,7 +2405,7 @@ function close_all_drawers(gobj)
|
|
|
2011
2405
|
************************************************************/
|
|
2012
2406
|
function show_stage_placeholder(gobj, stage_name, message)
|
|
2013
2407
|
{
|
|
2014
|
-
let priv =
|
|
2408
|
+
let priv = gobj.priv;
|
|
2015
2409
|
let stage = priv.stages[stage_name];
|
|
2016
2410
|
if(!stage || !stage.el) {
|
|
2017
2411
|
return;
|
|
@@ -2027,7 +2421,7 @@ function show_stage_placeholder(gobj, stage_name, message)
|
|
|
2027
2421
|
|
|
2028
2422
|
function clear_stage_placeholder(gobj, stage_name)
|
|
2029
2423
|
{
|
|
2030
|
-
let priv =
|
|
2424
|
+
let priv = gobj.priv;
|
|
2031
2425
|
let stage = priv.stages[stage_name];
|
|
2032
2426
|
if(!stage || !stage.el) {
|
|
2033
2427
|
return;
|
|
@@ -2040,7 +2434,7 @@ function clear_stage_placeholder(gobj, stage_name)
|
|
|
2040
2434
|
|
|
2041
2435
|
function update_secondary_nav_visibility(gobj, entry)
|
|
2042
2436
|
{
|
|
2043
|
-
let priv =
|
|
2437
|
+
let priv = gobj.priv;
|
|
2044
2438
|
/* shell.routes entries (root, forms, action routes) have no menu
|
|
2045
2439
|
* item — entry.item / entry.parent_item are null. No active
|
|
2046
2440
|
* primary then: the secondary zone collapses (has_secondary
|
|
@@ -2135,8 +2529,10 @@ function ac_nav_clicked(gobj, event, kw, src)
|
|
|
2135
2529
|
|
|
2136
2530
|
/* When hash routing is on, let the hash drive navigate_to() — that
|
|
2137
2531
|
* way back/forward buttons and programmatic hash changes all flow
|
|
2138
|
-
* through the same code path. Otherwise call navigate_to directly.
|
|
2139
|
-
|
|
2532
|
+
* through the same code path. Otherwise call navigate_to directly.
|
|
2533
|
+
* Exception: URL-restoring action routes skip the push — see
|
|
2534
|
+
* route_restores_url. */
|
|
2535
|
+
if(gobj_read_attr(gobj, "use_hash") && !route_restores_url(gobj, route)) {
|
|
2140
2536
|
let target_hash = route_to_hash(route);
|
|
2141
2537
|
if(window.location.hash !== target_hash) {
|
|
2142
2538
|
window.location.hash = target_hash; /* fires hashchange */
|
|
@@ -2226,7 +2622,7 @@ function find_primary_item(shell_gobj, menu_id, item_id)
|
|
|
2226
2622
|
/* Drop a route from the index and destroy any mounted view for it. */
|
|
2227
2623
|
function prune_route(shell_gobj, route)
|
|
2228
2624
|
{
|
|
2229
|
-
let priv =
|
|
2625
|
+
let priv = shell_gobj.priv;
|
|
2230
2626
|
delete priv.item_index[route];
|
|
2231
2627
|
for(let stage_name in priv.stages) {
|
|
2232
2628
|
let stage = priv.stages[stage_name];
|
|
@@ -2253,7 +2649,7 @@ function prune_route(shell_gobj, route)
|
|
|
2253
2649
|
|
|
2254
2650
|
function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
|
|
2255
2651
|
{
|
|
2256
|
-
let priv =
|
|
2652
|
+
let priv = shell_gobj.priv;
|
|
2257
2653
|
if(!priv) {
|
|
2258
2654
|
return -1;
|
|
2259
2655
|
}
|
|
@@ -2283,8 +2679,11 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
|
|
|
2283
2679
|
let new_set = {};
|
|
2284
2680
|
for(let it of items) {
|
|
2285
2681
|
if(it && it.route) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2682
|
+
/* Same canonical form as build_item_index: requests are
|
|
2683
|
+
* normalized, so raw keys would never match their clicks. */
|
|
2684
|
+
let it_route = normalize_route(it.route);
|
|
2685
|
+
new_routes.push(it_route);
|
|
2686
|
+
new_set[it_route] = true;
|
|
2288
2687
|
}
|
|
2289
2688
|
}
|
|
2290
2689
|
for(let route of prev_routes) {
|
|
@@ -2298,7 +2697,7 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
|
|
|
2298
2697
|
if(!it || !it.route) {
|
|
2299
2698
|
continue;
|
|
2300
2699
|
}
|
|
2301
|
-
priv.item_index[it.route] = {
|
|
2700
|
+
priv.item_index[normalize_route(it.route)] = {
|
|
2302
2701
|
item: it,
|
|
2303
2702
|
parent_item: parent_item,
|
|
2304
2703
|
stage: (it.target && it.target.stage) || null,
|
|
@@ -2388,6 +2787,17 @@ function create_gclass(gclass_name)
|
|
|
2388
2787
|
|event_flag_t.EVF_PUBLIC_EVENT
|
|
2389
2788
|
|event_flag_t.EVF_NO_WARN_SUBS],
|
|
2390
2789
|
["EV_ROUTE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT
|
|
2790
|
+
|event_flag_t.EVF_PUBLIC_EVENT
|
|
2791
|
+
|event_flag_t.EVF_NO_WARN_SUBS],
|
|
2792
|
+
|
|
2793
|
+
/* The language changed (the app switched it and called
|
|
2794
|
+
* yui_shell_language_changed). refresh_language() re-translates
|
|
2795
|
+
* every node that CARRIES its key, but a view that COMPOSED a string
|
|
2796
|
+
* with t() at render time — a title, a row counter, a Tabulator
|
|
2797
|
+
* header or paginator — holds no key and cannot be reached that way.
|
|
2798
|
+
* So the fact is published: such a view subscribes to its shell and
|
|
2799
|
+
* re-renders its own translated parts. */
|
|
2800
|
+
["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT
|
|
2391
2801
|
|event_flag_t.EVF_PUBLIC_EVENT
|
|
2392
2802
|
|event_flag_t.EVF_NO_WARN_SUBS]
|
|
2393
2803
|
];
|
|
@@ -2442,13 +2852,96 @@ function yui_shell_of(gobj)
|
|
|
2442
2852
|
}
|
|
2443
2853
|
|
|
2444
2854
|
/************************************************************
|
|
2445
|
-
* Programmatic navigation
|
|
2855
|
+
* Programmatic navigation.
|
|
2856
|
+
*
|
|
2857
|
+
* Default — PUSH: the user MOVED somewhere new, so change the URL via
|
|
2858
|
+
* the hash and let the browser record a Back entry (routed through the
|
|
2859
|
+
* same hashchange path as a nav click). This is the common case and the
|
|
2860
|
+
* safe default: a human deliberately chose to go there, so Back must
|
|
2861
|
+
* return them.
|
|
2862
|
+
*
|
|
2863
|
+
* `opts.replace:true` — REPLACE: sync the URL WITHOUT a Back entry. Use
|
|
2864
|
+
* when CODE decided the move for the user: redirects, normalizations,
|
|
2865
|
+
* submenu-parent → default child, F5-restores (see ROUTING.md §2/§7).
|
|
2866
|
+
*
|
|
2867
|
+
* `opts.push:true` is redundant now (it IS the default) but stays valid,
|
|
2868
|
+
* so migrated call sites keep documenting their intent explicitly.
|
|
2446
2869
|
************************************************************/
|
|
2447
|
-
function yui_shell_navigate(shell_gobj, route)
|
|
2870
|
+
function yui_shell_navigate(shell_gobj, route, opts)
|
|
2448
2871
|
{
|
|
2872
|
+
if(!(opts && opts.replace) && gobj_read_attr(shell_gobj, "use_hash")) {
|
|
2873
|
+
let target_hash = route_to_hash(route);
|
|
2874
|
+
if(window.location.hash !== target_hash) {
|
|
2875
|
+
/* Push a real history entry; the hashchange handler mounts. */
|
|
2876
|
+
window.location.hash = target_hash;
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
/* Same hash — hashchange won't fire; fall through to mount. */
|
|
2880
|
+
}
|
|
2449
2881
|
navigate_to(shell_gobj, route);
|
|
2450
2882
|
}
|
|
2451
2883
|
|
|
2884
|
+
/************************************************************
|
|
2885
|
+
* Nav map — the WHOLE navigation surface as an ordered tree, for
|
|
2886
|
+
* the "site map" / documentation viewer. The heavy lifting is the
|
|
2887
|
+
* pure builder in route_map_model.js (unit-tested there): toolbar
|
|
2888
|
+
* (incl. the account dropdown), every declared menu (incl. live
|
|
2889
|
+
* dynamic tabs), each view's contributed sub-routes, the routes
|
|
2890
|
+
* declared only in the route table (`other`), and the "you are
|
|
2891
|
+
* here" marker. Returns {brand, toolbar, nav, other}.
|
|
2892
|
+
************************************************************/
|
|
2893
|
+
function yui_shell_nav_map(shell_gobj)
|
|
2894
|
+
{
|
|
2895
|
+
let priv = shell_gobj.priv;
|
|
2896
|
+
return build_nav_map({
|
|
2897
|
+
config: gobj_read_attr(shell_gobj, "config") || {},
|
|
2898
|
+
item_index: (priv && priv.item_index) || {},
|
|
2899
|
+
sub_routes: (priv && priv.sub_routes) || {},
|
|
2900
|
+
event_handlers: (priv && priv.event_handlers) || {},
|
|
2901
|
+
current_route: gobj_read_attr(shell_gobj, "current_route") || ""
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
/************************************************************
|
|
2906
|
+
* Sub-route contributor protocol (ROUTING.md): a mounted view
|
|
2907
|
+
* declares the deep, view-owned children of its base route for the
|
|
2908
|
+
* site map. `nodes` is an ordered array of
|
|
2909
|
+
* {route, label, icon?, children?} (full hashless routes); pass
|
|
2910
|
+
* null/[] to clear (call on the view's stop/teardown so the map
|
|
2911
|
+
* never shows a torn-down view's children).
|
|
2912
|
+
************************************************************/
|
|
2913
|
+
function yui_shell_set_sub_routes(shell_gobj, base_route, nodes)
|
|
2914
|
+
{
|
|
2915
|
+
let priv = shell_gobj.priv;
|
|
2916
|
+
if(!priv || !priv.sub_routes || empty_string(base_route)) {
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
if(nodes && nodes.length) {
|
|
2920
|
+
priv.sub_routes[base_route] = nodes;
|
|
2921
|
+
} else {
|
|
2922
|
+
delete priv.sub_routes[base_route];
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
/************************************************************
|
|
2927
|
+
* Event-handler registry (ROUTING.md): a gclass that HANDLES a
|
|
2928
|
+
* toolbar/account action event self-declares, so the site map can
|
|
2929
|
+
* show where the action is implemented. Call once (e.g. in mt_start,
|
|
2930
|
+
* next to the matching `gobj_subscribe_event`). Idempotent per
|
|
2931
|
+
* (event, gclass); several gclasses may handle the same event.
|
|
2932
|
+
************************************************************/
|
|
2933
|
+
function yui_shell_register_event_handler(shell_gobj, event, gclass)
|
|
2934
|
+
{
|
|
2935
|
+
let priv = shell_gobj.priv;
|
|
2936
|
+
if(!priv || !priv.event_handlers || empty_string(event) || empty_string(gclass)) {
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
let list = priv.event_handlers[event] || (priv.event_handlers[event] = []);
|
|
2940
|
+
if(list.indexOf(gclass) < 0) {
|
|
2941
|
+
list.push(gclass);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2452
2945
|
/* Drawer helpers — toggle the off-canvas nav from the outside
|
|
2453
2946
|
* (e.g. a hamburger button in the toolbar). menu_id is optional. */
|
|
2454
2947
|
function yui_shell_open_drawer(shell_gobj, menu_id) { open_drawer(shell_gobj, menu_id); }
|
|
@@ -2477,6 +2970,45 @@ function yui_shell_pop_escape(shell_gobj, handler)
|
|
|
2477
2970
|
pop_escape(shell_gobj, handler);
|
|
2478
2971
|
}
|
|
2479
2972
|
|
|
2973
|
+
/* Overlay history integration — public API for overlays the shell does
|
|
2974
|
+
* not own (modals from shell_modals.js, floating C_YUI_WINDOW popups).
|
|
2975
|
+
*
|
|
2976
|
+
* let overlay = yui_shell_register_overlay(shell, close_fn);
|
|
2977
|
+
* // ... when the overlay closes by ANY non-Back path, also call:
|
|
2978
|
+
* yui_shell_overlay_dismissed(shell, overlay);
|
|
2979
|
+
*
|
|
2980
|
+
* `close_fn` is what the Back button invokes to tear the overlay down.
|
|
2981
|
+
* Returns null when history integration is off (no shell / use_hash) —
|
|
2982
|
+
* callers just skip the paired yui_shell_overlay_dismissed then. */
|
|
2983
|
+
function yui_shell_register_overlay(shell_gobj, close_fn)
|
|
2984
|
+
{
|
|
2985
|
+
return push_overlay_history(shell_gobj, close_fn);
|
|
2986
|
+
}
|
|
2987
|
+
function yui_shell_overlay_dismissed(shell_gobj, overlay)
|
|
2988
|
+
{
|
|
2989
|
+
overlay_dismissed(shell_gobj, overlay);
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/************************************************************
|
|
2993
|
+
* Take the URL back off an action route the app parked it on
|
|
2994
|
+
* (`redirect:"stay"`), from the overlay's close path — the
|
|
2995
|
+
* reference `stay` wiring (ROUTING.md §7.1).
|
|
2996
|
+
*
|
|
2997
|
+
* Guarded on purpose: when the close came from the shell's
|
|
2998
|
+
* overlay drain (the user navigated to another resting route
|
|
2999
|
+
* while the overlay was open), the URL has ALREADY moved off
|
|
3000
|
+
* the route — a blind history.back() there lands on the
|
|
3001
|
+
* stranded action entries and RE-FIRES the action, reopening
|
|
3002
|
+
* the overlay and hijacking the navigation the user asked
|
|
3003
|
+
* for. Only back() while the URL still sits on the route.
|
|
3004
|
+
************************************************************/
|
|
3005
|
+
function yui_shell_unpark_route(route)
|
|
3006
|
+
{
|
|
3007
|
+
if(window.location.hash === route_to_hash(normalize_route(route))) {
|
|
3008
|
+
window.history.back();
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
2480
3012
|
/************************************************************
|
|
2481
3013
|
* Avatar provider — toolbar items with type:"avatar" call the
|
|
2482
3014
|
* registered provider whenever the shell paints initials. The
|
|
@@ -2489,7 +3021,7 @@ function yui_shell_pop_escape(shell_gobj, handler)
|
|
|
2489
3021
|
************************************************************/
|
|
2490
3022
|
function yui_shell_set_avatar_provider(shell_gobj, provider)
|
|
2491
3023
|
{
|
|
2492
|
-
let priv =
|
|
3024
|
+
let priv = shell_gobj.priv;
|
|
2493
3025
|
if(!priv) {
|
|
2494
3026
|
return;
|
|
2495
3027
|
}
|
|
@@ -2513,13 +3045,35 @@ function yui_shell_refresh_avatars(shell_gobj)
|
|
|
2513
3045
|
************************************************************/
|
|
2514
3046
|
function yui_shell_set_translator(shell_gobj, t)
|
|
2515
3047
|
{
|
|
2516
|
-
let priv =
|
|
3048
|
+
let priv = shell_gobj.priv;
|
|
2517
3049
|
if(!priv) {
|
|
2518
3050
|
return;
|
|
2519
3051
|
}
|
|
2520
3052
|
priv.translator = (typeof t === "function") ? t : null;
|
|
2521
3053
|
}
|
|
2522
3054
|
|
|
3055
|
+
/************************************************************
|
|
3056
|
+
* The app switched the language: re-translate the whole document (every
|
|
3057
|
+
* node carrying data-i18n / data-i18n-title / data-i18n-aria-label) and
|
|
3058
|
+
* PUBLISH the fact, so the views that build DOM imperatively — Tabulator
|
|
3059
|
+
* headers and paginators, composed titles, row counters — can re-render
|
|
3060
|
+
* what no attribute can reach.
|
|
3061
|
+
*
|
|
3062
|
+
* The app remains the owner of the locales: it switches its i18next and
|
|
3063
|
+
* calls this. The shell only fans the fact out.
|
|
3064
|
+
************************************************************/
|
|
3065
|
+
function yui_shell_language_changed(shell_gobj)
|
|
3066
|
+
{
|
|
3067
|
+
if(!shell_gobj || !is_gobj(shell_gobj)) {
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
let priv = shell_gobj.priv;
|
|
3071
|
+
if(priv && typeof priv.translator === "function") {
|
|
3072
|
+
refresh_language(document.body, priv.translator);
|
|
3073
|
+
}
|
|
3074
|
+
gobj_publish_event(shell_gobj, "EV_LANGUAGE_CHANGED", {});
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2523
3077
|
/************************************************************
|
|
2524
3078
|
* Set the backend-connection state painted by every
|
|
2525
3079
|
* type:"connection" toolbar item. Host/event-driven: the
|
|
@@ -2529,7 +3083,7 @@ function yui_shell_set_translator(shell_gobj, t)
|
|
|
2529
3083
|
************************************************************/
|
|
2530
3084
|
function yui_shell_set_connection_state(shell_gobj, connected)
|
|
2531
3085
|
{
|
|
2532
|
-
let priv =
|
|
3086
|
+
let priv = shell_gobj.priv;
|
|
2533
3087
|
if(!priv || !is_array(priv.conn_nodes)) {
|
|
2534
3088
|
return;
|
|
2535
3089
|
}
|
|
@@ -2587,14 +3141,21 @@ export {
|
|
|
2587
3141
|
register_c_yui_shell,
|
|
2588
3142
|
yui_shell_of,
|
|
2589
3143
|
yui_shell_navigate,
|
|
3144
|
+
yui_shell_nav_map,
|
|
3145
|
+
yui_shell_set_sub_routes,
|
|
3146
|
+
yui_shell_register_event_handler,
|
|
2590
3147
|
yui_shell_open_drawer,
|
|
2591
3148
|
yui_shell_close_drawer,
|
|
2592
3149
|
yui_shell_toggle_drawer,
|
|
2593
3150
|
yui_shell_push_escape,
|
|
2594
3151
|
yui_shell_pop_escape,
|
|
3152
|
+
yui_shell_register_overlay,
|
|
3153
|
+
yui_shell_overlay_dismissed,
|
|
3154
|
+
yui_shell_unpark_route,
|
|
2595
3155
|
yui_shell_set_avatar_provider,
|
|
2596
3156
|
yui_shell_refresh_avatars,
|
|
2597
3157
|
yui_shell_set_translator,
|
|
3158
|
+
yui_shell_language_changed,
|
|
2598
3159
|
yui_shell_set_connection_state,
|
|
2599
3160
|
yui_shell_set_toolbar_item_icon,
|
|
2600
3161
|
yui_shell_close_dropdown,
|