@yuneta/gobj-ui 6.1.1 → 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:
@@ -539,12 +556,15 @@ schema, so the drawing can be held against the ASCII one in its `.c`.
539
556
  - Events: `EV_SHOW`, `EV_REBUILD`, `EV_THEME` (restyle — it repaints the G6
540
557
  graph in place, preserving the user's zoom/pan), plus `EV_NODE_CLICK`, which
541
558
  a node click sends into the FSM. **With a `node_route` the click IS a
542
- navigation** and this view makes it; **without one the click is published**
543
- (`{topic}`) for whoever mounted the view a host that draws the same picture
544
- inside its own screens opens the topic in place, with no hash involved. A
545
- host that subscribes must declare `EV_NODE_CLICK` in its own FSM, as with
546
- every event a child publishes. Since 6.1.0; before that a click with no route
547
- was dropped.
559
+ navigation** and this view makes it. **Without one the click is dropped,
560
+ unless the host asked for it with `with_node_click`** then it is published
561
+ as `{topic}`, for a host that draws the same picture inside its own screens
562
+ and opens the topic in place, with no hash involved. That host must declare
563
+ `EV_NODE_CLICK` in its own FSM, as with every event a child publishes, which
564
+ is exactly why it is opt-in: the CHILD subscription model subscribes a host
565
+ to ALL of this view's events, so publishing one unasked turns a click into
566
+ "Event NOT DEFINED in state" underneath a host that never wanted it. Since
567
+ 6.1.2 (6.1.0 and 6.1.1 published it unconditionally).
548
568
 
549
569
  Barrel-exported and public from 4.0.0. Renders with `@antv/g6`; the cards are
550
570
  HTML nodes carrying their own inline colours, so a theme switch repaints them in
@@ -19831,6 +19831,7 @@ var attrs_table$3 = [
19831
19831
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
19832
19832
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "descs", 0, null, "Treedb schema: {topic_name: desc}"),
19833
19833
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
19834
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, "With no `node_route`, PUBLISH a node click as EV_NODE_CLICK instead of dropping it. Opt-in, because a subscriber has to DECLARE the event: the hosts that route by hash, and the ones that want nothing, must not have it appear underneath them"),
19834
19835
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
19835
19836
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
19836
19837
  (0, _yuneta_gobj_js.SDATA_END)()
@@ -20235,19 +20236,26 @@ function destroy_graph(gobj) {
20235
20236
  * With a `node_route` the click IS a navigation and this view
20236
20237
  * makes it: that is what the route was handed down for.
20237
20238
  *
20238
- * Without one it is still an action, and it belongs to whoever
20239
- * mounted this view — the schema editor draws the same picture
20240
- * inside its own screens, where a topic opens in place and no
20241
- * hash is involved. So the click is published rather than
20242
- * dropped. A host that subscribes must DECLARE EV_NODE_CLICK in
20243
- * its own FSM, as with every event a child publishes.
20239
+ * Without one it is still an action, and it can belong to
20240
+ * whoever mounted this view — the schema editor draws the same
20241
+ * picture inside its own screens, where a topic opens in place
20242
+ * and no hash is involved. That host asks for it with
20243
+ * `with_node_click` and DECLARES EV_NODE_CLICK in its own FSM,
20244
+ * as with every event a child publishes.
20245
+ *
20246
+ * It is opt-in and not the default for exactly that reason: the
20247
+ * CHILD subscription model subscribes the host to ALL of this
20248
+ * view's events, so publishing one unasked turns a click into
20249
+ * "Event NOT DEFINED in state" in every host that never wanted
20250
+ * it — the topics view that mounts this as its schema landing,
20251
+ * and the offline demo that mounts it against a json file.
20244
20252
  ************************************************************/
20245
20253
  function ac_node_click$2(gobj, event, kw, src) {
20246
20254
  let topic = kw && kw.node_id;
20247
20255
  let route = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "node_route");
20248
20256
  if (!topic) return 0;
20249
20257
  if (!route) {
20250
- (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_NODE_CLICK", { topic });
20258
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_node_click")) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_NODE_CLICK", { topic });
20251
20259
  return 0;
20252
20260
  }
20253
20261
  let href = route.replace("{topic}", topic);
@@ -23415,6 +23423,7 @@ function render_diagram(gobj) {
23415
23423
  subscriber: gobj,
23416
23424
  descs,
23417
23425
  node_route: "",
23426
+ with_node_click: true,
23418
23427
  system: true
23419
23428
  }, gobj);
23420
23429
  if (!diagram) {
@@ -19818,6 +19818,7 @@ var attrs_table$3 = [
19818
19818
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
19819
19819
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Treedb schema: {topic_name: desc}"),
19820
19820
  SDATA(data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
19821
+ SDATA(data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, "With no `node_route`, PUBLISH a node click as EV_NODE_CLICK instead of dropping it. Opt-in, because a subscriber has to DECLARE the event: the hosts that route by hash, and the ones that want nothing, must not have it appear underneath them"),
19821
19822
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
19822
19823
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
19823
19824
  SDATA_END()
@@ -20222,19 +20223,26 @@ function destroy_graph(gobj) {
20222
20223
  * With a `node_route` the click IS a navigation and this view
20223
20224
  * makes it: that is what the route was handed down for.
20224
20225
  *
20225
- * Without one it is still an action, and it belongs to whoever
20226
- * mounted this view — the schema editor draws the same picture
20227
- * inside its own screens, where a topic opens in place and no
20228
- * hash is involved. So the click is published rather than
20229
- * dropped. A host that subscribes must DECLARE EV_NODE_CLICK in
20230
- * its own FSM, as with every event a child publishes.
20226
+ * Without one it is still an action, and it can belong to
20227
+ * whoever mounted this view — the schema editor draws the same
20228
+ * picture inside its own screens, where a topic opens in place
20229
+ * and no hash is involved. That host asks for it with
20230
+ * `with_node_click` and DECLARES EV_NODE_CLICK in its own FSM,
20231
+ * as with every event a child publishes.
20232
+ *
20233
+ * It is opt-in and not the default for exactly that reason: the
20234
+ * CHILD subscription model subscribes the host to ALL of this
20235
+ * view's events, so publishing one unasked turns a click into
20236
+ * "Event NOT DEFINED in state" in every host that never wanted
20237
+ * it — the topics view that mounts this as its schema landing,
20238
+ * and the offline demo that mounts it against a json file.
20231
20239
  ************************************************************/
20232
20240
  function ac_node_click$2(gobj, event, kw, src) {
20233
20241
  let topic = kw && kw.node_id;
20234
20242
  let route = gobj_read_str_attr(gobj, "node_route");
20235
20243
  if (!topic) return 0;
20236
20244
  if (!route) {
20237
- gobj_publish_event(gobj, "EV_NODE_CLICK", { topic });
20245
+ if (gobj_read_bool_attr(gobj, "with_node_click")) gobj_publish_event(gobj, "EV_NODE_CLICK", { topic });
20238
20246
  return 0;
20239
20247
  }
20240
20248
  let href = route.replace("{topic}", topic);
@@ -23402,6 +23410,7 @@ function render_diagram(gobj) {
23402
23410
  subscriber: gobj,
23403
23411
  descs,
23404
23412
  node_route: "",
23413
+ with_node_click: true,
23405
23414
  system: true
23406
23415
  }, gobj);
23407
23416
  if (!diagram) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "6.1.1",
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. */
@@ -1389,9 +1389,11 @@ function render_diagram(gobj)
1389
1389
  subscriber: gobj,
1390
1390
  descs: descs,
1391
1391
  /* No `node_route`: this diagram lives inside the editor and
1392
- * a node click is answered here, not by a hash the shell
1393
- * would have to own. */
1394
- node_route: "",
1392
+ * a node click is answered HERE, not by a hash the shell
1393
+ * would have to own — which is what `with_node_click` asks
1394
+ * for, and why EV_NODE_CLICK is declared in this FSM. */
1395
+ node_route: "",
1396
+ with_node_click: true,
1395
1397
  system: true
1396
1398
  },
1397
1399
  gobj
@@ -94,6 +94,7 @@ const attrs_table = [
94
94
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
95
95
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Treedb schema: {topic_name: desc}"),
96
96
  SDATA(data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
97
+ SDATA(data_type_t.DTP_BOOLEAN, "with_node_click", 0, false, "With no `node_route`, PUBLISH a node click as EV_NODE_CLICK instead of dropping it. Opt-in, because a subscriber has to DECLARE the event: the hosts that route by hash, and the ones that want nothing, must not have it appear underneath them"),
97
98
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
98
99
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
99
100
  SDATA_END()
@@ -622,12 +623,19 @@ function destroy_graph(gobj)
622
623
  * With a `node_route` the click IS a navigation and this view
623
624
  * makes it: that is what the route was handed down for.
624
625
  *
625
- * Without one it is still an action, and it belongs to whoever
626
- * mounted this view — the schema editor draws the same picture
627
- * inside its own screens, where a topic opens in place and no
628
- * hash is involved. So the click is published rather than
629
- * dropped. A host that subscribes must DECLARE EV_NODE_CLICK in
630
- * its own FSM, as with every event a child publishes.
626
+ * Without one it is still an action, and it can belong to
627
+ * whoever mounted this view — the schema editor draws the same
628
+ * picture inside its own screens, where a topic opens in place
629
+ * and no hash is involved. That host asks for it with
630
+ * `with_node_click` and DECLARES EV_NODE_CLICK in its own FSM,
631
+ * as with every event a child publishes.
632
+ *
633
+ * It is opt-in and not the default for exactly that reason: the
634
+ * CHILD subscription model subscribes the host to ALL of this
635
+ * view's events, so publishing one unasked turns a click into
636
+ * "Event NOT DEFINED in state" in every host that never wanted
637
+ * it — the topics view that mounts this as its schema landing,
638
+ * and the offline demo that mounts it against a json file.
631
639
  ************************************************************/
632
640
  function ac_node_click(gobj, event, kw, src)
633
641
  {
@@ -638,7 +646,9 @@ function ac_node_click(gobj, event, kw, src)
638
646
  return 0;
639
647
  }
640
648
  if(!route) {
641
- gobj_publish_event(gobj, "EV_NODE_CLICK", {topic: topic});
649
+ if(gobj_read_bool_attr(gobj, "with_node_click")) {
650
+ gobj_publish_event(gobj, "EV_NODE_CLICK", {topic: topic});
651
+ }
642
652
  return 0;
643
653
  }
644
654
  let href = route.replace("{topic}", topic);
@@ -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
+ });