@yuneta/gobj-ui 6.1.2 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "6.1.2",
3
+ "version": "6.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
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 = {};
@@ -652,7 +657,16 @@ function render_projection(gobj, mode, $where, active_route, active_id, zones_on
652
657
  disabled: gobj_read_attr(child, "disabled")
653
658
  });
654
659
  }
655
- let items = child_nav_items(specs, (id) => `${base}/${id}`);
660
+ /* A nav item points at the child's canonical route, or when this
661
+ * tree remembers — at where the operator left that child. Same
662
+ * contract either way: what the item carries is a real position,
663
+ * so clicking it is a navigation and not a restore that would then
664
+ * have to argue with the url. */
665
+ let remember = gobj_read_bool_attr(gobj, "remember_position");
666
+ let items = child_nav_items(specs, (id) => {
667
+ let route = `${base}/${id}`;
668
+ return remember ? nav_route_with_tail(route, priv.remembered[id]) : route;
669
+ });
656
670
 
657
671
  let i = 0;
658
672
  for(let render of renders) {
@@ -714,7 +728,11 @@ function project_into_zone(gobj, render, items, active_route, active_id)
714
728
  {
715
729
  let priv = gobj.priv;
716
730
  let key = `${render.zone}|${render.layout}|${render.show_on || ""}`;
717
- let sig = items.map((it) => it.id).join(",");
731
+ /* The route is part of it: with `remember_position` an item's id
732
+ * never changes while its destination does, so a signature of ids
733
+ * alone would leave a persisted zone nav pointing at where the
734
+ * operator was three navigations ago. */
735
+ let sig = items.map((it) => `${it.id}=${it.route}`).join(",");
718
736
  let nav = priv.zone_navs[key];
719
737
 
720
738
  if(!nav) {
@@ -989,6 +1007,18 @@ function activate(gobj, subpath, inherited_depth)
989
1007
  priv.chrome_depth = info.chrome_depth;
990
1008
  priv.chain = info.chain;
991
1009
 
1010
+ /* WHERE THE OPERATOR IS INSIDE THIS CHILD, recorded before anything
1011
+ * is drawn, because the strip drawn on this very activation is the
1012
+ * one that has to point back here.
1013
+ *
1014
+ * An empty tail is recorded too, and that is not the same as not
1015
+ * recording: navigating to a child's bare route IS the operator
1016
+ * choosing its home, and the item must stop pointing at the topic
1017
+ * they left three navigations ago. */
1018
+ if(head) {
1019
+ priv.remembered[head] = tail || "";
1020
+ }
1021
+
992
1022
  /* Structure ends here: whatever is left of the url is data, and it
993
1023
  * belongs to the viewer. Re-render only the first time — paging
994
1024
  * through data must not rebuild the DOM under the viewer. */
@@ -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
+ });