@yuneta/gobj-ui 6.1.2 → 6.2.1
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 +17 -0
- package/package.json +1 -1
- package/src/c_yui_node.js +53 -3
- package/src/node_tree_model.js +26 -0
- package/src/node_tree_model.test.js +31 -1
package/README.md
CHANGED
|
@@ -201,6 +201,23 @@ seen*:
|
|
|
201
201
|
`projection.path`) while the rest of the tree keeps its tabs. Note the
|
|
202
202
|
asymmetry that makes it a third mode and not a layout: `index` and `chrome`
|
|
203
203
|
project a node's CHILDREN; `path` projects the way in.
|
|
204
|
+
- **`remember_position` — an item points at where you LEFT that child.**
|
|
205
|
+
Without it a nav item points at the canonical route of its child, so a strip
|
|
206
|
+
of children behaves like a row of tabs that forgets: open a topic inside one,
|
|
207
|
+
move to a sibling, come back, and the tab is at its landing — browser Back
|
|
208
|
+
the only way to what was open. With it, the item carries the tail that was
|
|
209
|
+
last active under that child.
|
|
210
|
+
|
|
211
|
+
It stays a **real position**, which is the reason it is done here and not by
|
|
212
|
+
the viewer restoring itself: clicking is a navigation like any other, nothing
|
|
213
|
+
redirects and nothing argues with the url. A bare navigation to a child
|
|
214
|
+
records "its home", so choosing the landing sticks too.
|
|
215
|
+
|
|
216
|
+
Off by default: a tree whose children are pages wants the item to BE the
|
|
217
|
+
destination. On for a tree whose children are workspaces with a position
|
|
218
|
+
inside them — the agent console's strip of treedbs, each with its open topic.
|
|
219
|
+
Since 6.2.0.
|
|
220
|
+
|
|
204
221
|
- **`nav_mode` — the three shapes as one runtime knob.** The two bullets above
|
|
205
222
|
describe what a tree *declares*; `nav_mode` is how a user *chooses* between
|
|
206
223
|
the shapes without the app rewriting anything:
|
package/package.json
CHANGED
package/src/c_yui_node.js
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
gobj_parent,
|
|
107
107
|
gobj_gclass_name,
|
|
108
108
|
gobj_read_attr,
|
|
109
|
+
gobj_read_bool_attr,
|
|
109
110
|
gobj_read_pointer_attr,
|
|
110
111
|
gobj_write_attr,
|
|
111
112
|
gobj_subscribe_event,
|
|
@@ -124,6 +125,7 @@ import {
|
|
|
124
125
|
join_route,
|
|
125
126
|
projection_renders,
|
|
126
127
|
child_nav_items,
|
|
128
|
+
nav_route_with_tail,
|
|
127
129
|
chrome_visible,
|
|
128
130
|
normalize_spec,
|
|
129
131
|
is_nav_mode,
|
|
@@ -175,6 +177,7 @@ SDATA(data_type_t.DTP_JSON, "children", 0, null, "Declared child spec
|
|
|
175
177
|
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "ROOT only: the declared route the whole tree hangs from"),
|
|
176
178
|
SDATA(data_type_t.DTP_STRING, "tree_version", 0, "", "ROOT only: version of the tree CONTRACT (its paths are public urls)"),
|
|
177
179
|
SDATA(data_type_t.DTP_STRING, "nav_mode", 0, "stack","ROOT only: how the way in is shown — stack|back|path"),
|
|
180
|
+
SDATA(data_type_t.DTP_BOOLEAN, "remember_position", 0, false, "A nav item points at WHERE THE OPERATOR LEFT that child, not at its bare route: the tail last active under each child is remembered and appended. For a tree whose children are workspaces with a position inside them (a treedb and its open topic); off for one whose children are pages, where the item IS the destination"),
|
|
178
181
|
|
|
179
182
|
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement (shell view contract)"),
|
|
180
183
|
SDATA_END()
|
|
@@ -187,6 +190,7 @@ let PRIVATE_DATA = {
|
|
|
187
190
|
zone_sig: null, /* their last item signature */
|
|
188
191
|
content_gobj: null,
|
|
189
192
|
active_child: null, /* child NODE gobj currently on the path */
|
|
193
|
+
remembered: null, /* child id -> tail last active under it */
|
|
190
194
|
chrome_depth: null, /* effective for the active path (null = all) */
|
|
191
195
|
distance: 0, /* segments from me down to the tip */
|
|
192
196
|
chain: null, /* nodes from my child down to the tip */
|
|
@@ -224,6 +228,7 @@ function mt_create(gobj)
|
|
|
224
228
|
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
225
229
|
|
|
226
230
|
priv.children = [];
|
|
231
|
+
priv.remembered = {};
|
|
227
232
|
priv.navs = [];
|
|
228
233
|
priv.zone_navs = {};
|
|
229
234
|
priv.zone_sig = {};
|
|
@@ -627,6 +632,27 @@ function clear_navs(gobj)
|
|
|
627
632
|
* "index" — I am the tip: the projection is the page.
|
|
628
633
|
* "chrome" — a child is showing: the projection is its chrome.
|
|
629
634
|
************************************************************/
|
|
635
|
+
/************************************************************
|
|
636
|
+
* WHERE A CHILD'S NAV ITEM POINTS.
|
|
637
|
+
*
|
|
638
|
+
* One function, because two things read it and they must not
|
|
639
|
+
* disagree: the item's own href, and the `active_route` the nav
|
|
640
|
+
* is told to highlight — C_YUI_NAV matches by EXACT route, so an
|
|
641
|
+
* item pointing at a remembered position while the active route
|
|
642
|
+
* stayed canonical is an item that is never marked active.
|
|
643
|
+
************************************************************/
|
|
644
|
+
function child_nav_route(gobj, id)
|
|
645
|
+
{
|
|
646
|
+
let my_route = route_of(gobj);
|
|
647
|
+
let base = my_route === "/" ? "" : my_route;
|
|
648
|
+
let route = `${base}/${id}`;
|
|
649
|
+
|
|
650
|
+
if(!gobj_read_bool_attr(gobj, "remember_position")) {
|
|
651
|
+
return route;
|
|
652
|
+
}
|
|
653
|
+
return nav_route_with_tail(route, gobj.priv.remembered[id]);
|
|
654
|
+
}
|
|
655
|
+
|
|
630
656
|
function render_projection(gobj, mode, $where, active_route, active_id, zones_only)
|
|
631
657
|
{
|
|
632
658
|
let priv = gobj.priv;
|
|
@@ -652,7 +678,12 @@ function render_projection(gobj, mode, $where, active_route, active_id, zones_on
|
|
|
652
678
|
disabled: gobj_read_attr(child, "disabled")
|
|
653
679
|
});
|
|
654
680
|
}
|
|
655
|
-
|
|
681
|
+
/* A nav item points at the child's canonical route, or — when this
|
|
682
|
+
* tree remembers — at where the operator left that child. Same
|
|
683
|
+
* contract either way: what the item carries is a real position,
|
|
684
|
+
* so clicking it is a navigation and not a restore that would then
|
|
685
|
+
* have to argue with the url. */
|
|
686
|
+
let items = child_nav_items(specs, (id) => child_nav_route(gobj, id));
|
|
656
687
|
|
|
657
688
|
let i = 0;
|
|
658
689
|
for(let render of renders) {
|
|
@@ -714,7 +745,11 @@ function project_into_zone(gobj, render, items, active_route, active_id)
|
|
|
714
745
|
{
|
|
715
746
|
let priv = gobj.priv;
|
|
716
747
|
let key = `${render.zone}|${render.layout}|${render.show_on || ""}`;
|
|
717
|
-
|
|
748
|
+
/* The route is part of it: with `remember_position` an item's id
|
|
749
|
+
* never changes while its destination does, so a signature of ids
|
|
750
|
+
* alone would leave a persisted zone nav pointing at where the
|
|
751
|
+
* operator was three navigations ago. */
|
|
752
|
+
let sig = items.map((it) => `${it.id}=${it.route}`).join(",");
|
|
718
753
|
let nav = priv.zone_navs[key];
|
|
719
754
|
|
|
720
755
|
if(!nav) {
|
|
@@ -874,7 +909,10 @@ function render_child(gobj, child)
|
|
|
874
909
|
* level. `chrome_depth` is how a node caps that for its corner
|
|
875
910
|
* of the tree (see resolve_chrome_depth). */
|
|
876
911
|
if(chrome_visible(priv.distance, priv.chrome_depth)) {
|
|
877
|
-
|
|
912
|
+
/* The SAME route the item carries, or the nav has nothing to
|
|
913
|
+
* match: it highlights by exact route. */
|
|
914
|
+
render_projection(gobj, "chrome", priv.$chrome,
|
|
915
|
+
child_nav_route(gobj, gobj_read_attr(child, "node_id")),
|
|
878
916
|
gobj_read_attr(child, "node_id"));
|
|
879
917
|
}
|
|
880
918
|
render_path(gobj, priv.$chrome);
|
|
@@ -989,6 +1027,18 @@ function activate(gobj, subpath, inherited_depth)
|
|
|
989
1027
|
priv.chrome_depth = info.chrome_depth;
|
|
990
1028
|
priv.chain = info.chain;
|
|
991
1029
|
|
|
1030
|
+
/* WHERE THE OPERATOR IS INSIDE THIS CHILD, recorded before anything
|
|
1031
|
+
* is drawn, because the strip drawn on this very activation is the
|
|
1032
|
+
* one that has to point back here.
|
|
1033
|
+
*
|
|
1034
|
+
* An empty tail is recorded too, and that is not the same as not
|
|
1035
|
+
* recording: navigating to a child's bare route IS the operator
|
|
1036
|
+
* choosing its home, and the item must stop pointing at the topic
|
|
1037
|
+
* they left three navigations ago. */
|
|
1038
|
+
if(head) {
|
|
1039
|
+
priv.remembered[head] = tail || "";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
992
1042
|
/* Structure ends here: whatever is left of the url is data, and it
|
|
993
1043
|
* belongs to the viewer. Re-render only the first time — paging
|
|
994
1044
|
* through data must not rebuild the DOM under the viewer. */
|
package/src/node_tree_model.js
CHANGED
|
@@ -169,6 +169,32 @@ export function projection_renders(projection, mode)
|
|
|
169
169
|
* `route_of(id)` returns the canonical route of a child — the
|
|
170
170
|
* node supplies it, so this file stays route-agnostic.
|
|
171
171
|
************************************************************/
|
|
172
|
+
/***************************************************************
|
|
173
|
+
* nav_route_with_tail(route, tail) -> where that item goes
|
|
174
|
+
*
|
|
175
|
+
* A nav item normally points at the canonical route of a
|
|
176
|
+
* child. When the tree remembers positions it points at
|
|
177
|
+
* WHERE THE OPERATOR LEFT that child instead: the child's
|
|
178
|
+
* route plus the tail last active under it.
|
|
179
|
+
*
|
|
180
|
+
* It is the difference between a strip of treedbs that
|
|
181
|
+
* behaves like a row of tabs and one that drops what each
|
|
182
|
+
* tab had open every time you come back to it.
|
|
183
|
+
*
|
|
184
|
+
* An empty tail is not a position: it means the operator was
|
|
185
|
+
* at the child's own home, which is the canonical route.
|
|
186
|
+
***************************************************************/
|
|
187
|
+
export function nav_route_with_tail(route, tail)
|
|
188
|
+
{
|
|
189
|
+
let base = String(route || "");
|
|
190
|
+
let rest = String(tail || "").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
191
|
+
|
|
192
|
+
if(!rest) {
|
|
193
|
+
return base;
|
|
194
|
+
}
|
|
195
|
+
return base.replace(/\/+$/, "") + "/" + rest;
|
|
196
|
+
}
|
|
197
|
+
|
|
172
198
|
export function child_nav_items(children, route_of)
|
|
173
199
|
{
|
|
174
200
|
let items = [];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Unit tests for the pure node-tree helpers.
|
|
5
5
|
***********************************************************************/
|
|
6
|
-
import { test, expect } from "vitest";
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
7
|
import {
|
|
8
8
|
chrome_visible,
|
|
9
9
|
split_subpath,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
join_route,
|
|
12
12
|
projection_renders,
|
|
13
13
|
child_nav_items,
|
|
14
|
+
nav_route_with_tail,
|
|
14
15
|
normalize_spec,
|
|
15
16
|
is_nav_mode,
|
|
16
17
|
nav_mode_renders,
|
|
@@ -350,3 +351,32 @@ test("back is depth 1 and path is depth 0, whatever the tree declared", () => {
|
|
|
350
351
|
expect(nav_mode_depth(null, "stack")).toBe(null);
|
|
351
352
|
expect(nav_mode_depth(2, "stack")).toBe(2);
|
|
352
353
|
});
|
|
354
|
+
|
|
355
|
+
describe("nav_route_with_tail — a nav item that points where you left", () => {
|
|
356
|
+
test("no tail is the canonical route: the child's own home", () => {
|
|
357
|
+
expect(nav_route_with_tail("/schemas/db", "")).toBe("/schemas/db");
|
|
358
|
+
expect(nav_route_with_tail("/schemas/db", null)).toBe("/schemas/db");
|
|
359
|
+
expect(nav_route_with_tail("/schemas/db", undefined)).toBe("/schemas/db");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a tail is appended, which is the whole point", () => {
|
|
363
|
+
expect(nav_route_with_tail("/schemas/db", "users")).toBe("/schemas/db/users");
|
|
364
|
+
expect(nav_route_with_tail("/schemas/db", "users/info"))
|
|
365
|
+
.toBe("/schemas/db/users/info");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("it never doubles a slash, whichever side carries one", () => {
|
|
369
|
+
expect(nav_route_with_tail("/schemas/db/", "users")).toBe("/schemas/db/users");
|
|
370
|
+
expect(nav_route_with_tail("/schemas/db", "/users")).toBe("/schemas/db/users");
|
|
371
|
+
expect(nav_route_with_tail("/schemas/db/", "/users/")).toBe("/schemas/db/users");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("a tail of only slashes is no tail", () => {
|
|
375
|
+
expect(nav_route_with_tail("/schemas/db", "/")).toBe("/schemas/db");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("no route at all is not a crash", () => {
|
|
379
|
+
expect(nav_route_with_tail("", "users")).toBe("/users");
|
|
380
|
+
expect(nav_route_with_tail(null, null)).toBe("");
|
|
381
|
+
});
|
|
382
|
+
});
|