@yuneta/gobj-ui 7.23.69 → 7.23.71

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.
@@ -6398,6 +6398,57 @@ function plan_toolbar(wanted) {
6398
6398
  };
6399
6399
  }
6400
6400
  //#endregion
6401
+ //#region src/form_field_readonly.js
6402
+ /***********************************************************************
6403
+ * form_field_readonly.js
6404
+ *
6405
+ * Is this field of the form read-only?
6406
+ *
6407
+ * Pure, so the rule can be tested without a DOM: the gclass turns
6408
+ * the answer into `readonly`/`disabled` on a control, this decides
6409
+ * the answer. Same split as form_toolbar_plan.js and
6410
+ * treedb_write_plan.js.
6411
+ *
6412
+ * The rule it exists to state ONCE: **`writable` governs the WRITE
6413
+ * of a column's VALUE, and an fkey is not written — it is LINKED.**
6414
+ * An fkey column is normally declared with no `writable` flag at
6415
+ * all (`treedb_authzs`'s `users.roles` is `['fkey']`, and so is
6416
+ * almost every fkey in the tree), so reading "not writable" as "not
6417
+ * editable" takes away the only way to link a record to its parent
6418
+ * — with `users.roles`, the only way to give a person a role.
6419
+ *
6420
+ * The other half of this rule is already written down in
6421
+ * c_yui_treedb_topic_with_form.js, where the record travels back:
6422
+ * the form sends "the writable cols, the fkeys (a link is edited by
6423
+ * linking) and the pkey". The two halves have to agree, and when
6424
+ * they did not, the form disabled the control whose value the save
6425
+ * path was still waiting for.
6426
+ *
6427
+ * A `file` column is an fkey too (flagged `['fkey','file']`) and is
6428
+ * NOT covered: it answers `type: "file"`, its bytes travel beside
6429
+ * the record, and the SDK declares as legal a `file` column that
6430
+ * only a load fills.
6431
+ *
6432
+ * Copyright (c) 2026, ArtGins.
6433
+ * All Rights Reserved.
6434
+ ***********************************************************************/
6435
+ /***************************************************************
6436
+ * field_is_readonly(form_readonly, field_desc) -> boolean
6437
+ *
6438
+ * form_readonly the FORM is read-only: a record opened to be
6439
+ * looked at, or a store nobody may write. It is
6440
+ * a property of the opening, not of the schema,
6441
+ * and it wins over everything.
6442
+ * field_desc as treedb_get_field_desc() answers it
6443
+ * (`type`, `is_writable`).
6444
+ ***************************************************************/
6445
+ function field_is_readonly(form_readonly, field_desc) {
6446
+ if (form_readonly) return true;
6447
+ let desc = field_desc || {};
6448
+ if (desc.type === "fkey") return false;
6449
+ return !desc.is_writable;
6450
+ }
6451
+ //#endregion
6401
6452
  //#region src/yui_theme.js
6402
6453
  /***********************************************************************
6403
6454
  * yui_theme.js
@@ -7210,7 +7261,7 @@ function build_form_field_conf(gobj, field_desc) {
7210
7261
  placeholder: "",
7211
7262
  options: [],
7212
7263
  extras: {},
7213
- readonly: gobj_read_bool_attr(gobj, "readonly") || !field_desc.is_writable,
7264
+ readonly: field_is_readonly(gobj_read_bool_attr(gobj, "readonly"), field_desc),
7214
7265
  required: field_desc.is_required,
7215
7266
  hidden: field_desc.is_hidden,
7216
7267
  default_value: field_desc.default_value
@@ -27309,6 +27360,329 @@ function fold_reveal(model, state, node_key) {
27309
27360
  return true;
27310
27361
  }
27311
27362
  //#endregion
27363
+ //#region src/treedb_layout.js
27364
+ /***********************************************************************
27365
+ * treedb_layout.js
27366
+ *
27367
+ * Two layouts made for a treedb, and nothing else.
27368
+ *
27369
+ * What a treedb graph IS, once it is folded (treedb_fold_model.js):
27370
+ * a forest read left to right. A main topic is a tree (places:
27371
+ * country, region, site, hall), the other topics hang from its
27372
+ * nodes through hooks (devices, users, controllers), a record may
27373
+ * hang from two parents (a device from its place AND its
27374
+ * controller), and every visible node was reached from a root.
27375
+ * Tens to a few hundred cards on screen, never the whole store.
27376
+ *
27377
+ * G6's own layouts either do not know that or pay for not knowing:
27378
+ *
27379
+ * - `dagre` gets the reading direction right and then ranks by
27380
+ * longest path and minimises crossings with a heuristic that
27381
+ * REORDERS the siblings on every run -- open one hook and the
27382
+ * column next to it shuffles. It is O(n·m) per sweep and it is
27383
+ * the one thing that takes time on a big expansion.
27384
+ * - the tree layouts (`compact-box`, `indented`, `dendrogram`,
27385
+ * `mindmap`) draw exactly the shape wanted, but G6 runs them on
27386
+ * a TREE it builds from the edges: a node with two parents is
27387
+ * placed by whichever parent the builder met first, unstably.
27388
+ * - the force family scatters a tree, which is the pile with
27389
+ * physics.
27390
+ *
27391
+ * So: a tidy tree (`tree`) and an outline (`outline`), both fed by
27392
+ * the SAME spanning tree, chosen deterministically:
27393
+ *
27394
+ * - roots are the nodes with no incoming edge, in node order;
27395
+ * - a node belongs to the FIRST parent that reaches it in a
27396
+ * breadth-first walk from the roots -- the place, not the
27397
+ * controller, because the place's column came first -- and the
27398
+ * other links are drawn as edges across the tree;
27399
+ * - the children of a node are ordered by the HOOK they hang from
27400
+ * (the parent's port order, which is the schema's column order)
27401
+ * and then by node order (record order, the pages' order);
27402
+ * - a node nothing reaches (a cycle) is a root too.
27403
+ *
27404
+ * `tree`: the classic tidy tree, left to right. Each depth is a
27405
+ * column as wide as its widest card; a node is centred on the
27406
+ * block of its children; siblings stack with `nodesep` between
27407
+ * them. O(n), and opening a hook moves NOTHING that is not below
27408
+ * or beside it -- the cards keep their order, the eye keeps its
27409
+ * place.
27410
+ *
27411
+ * `outline`: one node per row, indented by depth -- what a JSON
27412
+ * viewer draws, and what a treedb is when read as one. Tall, but
27413
+ * nothing is ever beside anything: the reader scrolls a list.
27414
+ *
27415
+ * Pure: takes plain nodes and edges, returns positions. The G6
27416
+ * classes in c_g6_nodes_tree.js are two thin adapters over it.
27417
+ *
27418
+ * Copyright (c) 2026, ArtGins.
27419
+ * All Rights Reserved.
27420
+ ***********************************************************************/
27421
+ var DEFAULTS = {
27422
+ nodesep: 18,
27423
+ ranksep: 110,
27424
+ indent: 48
27425
+ };
27426
+ /************************************************************
27427
+ * The spanning tree of a graph, deterministic.
27428
+ *
27429
+ * nodes [{id, w, h, order}] order: creation index
27430
+ * edges [{source, target, rank}] rank: the hook's position in
27431
+ * the parent's ports (0 = first)
27432
+ *
27433
+ * Returns {roots: [id], children: Map<id, [id]>, depth: Map<id, n>,
27434
+ * by_id: Map<id, node>}
27435
+ ************************************************************/
27436
+ function spanning_tree(nodes, edges) {
27437
+ let by_id = /* @__PURE__ */ new Map();
27438
+ for (let i = 0; i < nodes.length; i++) {
27439
+ let n = nodes[i];
27440
+ by_id.set(n.id, Object.assign({ order: i }, n));
27441
+ }
27442
+ let out = /* @__PURE__ */ new Map();
27443
+ let has_parent = /* @__PURE__ */ new Set();
27444
+ for (let e of edges) {
27445
+ if (!by_id.has(e.source) || !by_id.has(e.target) || e.source === e.target) continue;
27446
+ if (!out.has(e.source)) out.set(e.source, []);
27447
+ out.get(e.source).push({
27448
+ id: e.target,
27449
+ rank: e.rank > 0 ? e.rank : 0
27450
+ });
27451
+ has_parent.add(e.target);
27452
+ }
27453
+ for (let list of out.values()) list.sort((a, b) => {
27454
+ if (a.rank !== b.rank) return a.rank - b.rank;
27455
+ return by_id.get(a.id).order - by_id.get(b.id).order;
27456
+ });
27457
+ let roots = [];
27458
+ for (let n of by_id.values()) if (!has_parent.has(n.id)) roots.push(n.id);
27459
+ let children = /* @__PURE__ */ new Map();
27460
+ let depth = /* @__PURE__ */ new Map();
27461
+ let assign = (start) => {
27462
+ let queue = [start];
27463
+ depth.set(start, 0);
27464
+ while (queue.length) {
27465
+ let id = queue.shift();
27466
+ let kids = [];
27467
+ for (let link of out.get(id) || []) {
27468
+ if (depth.has(link.id)) continue;
27469
+ depth.set(link.id, depth.get(id) + 1);
27470
+ kids.push(link.id);
27471
+ queue.push(link.id);
27472
+ }
27473
+ children.set(id, kids);
27474
+ }
27475
+ };
27476
+ for (let root of roots) assign(root);
27477
+ for (let n of by_id.values()) if (!depth.has(n.id)) {
27478
+ roots.push(n.id);
27479
+ assign(n.id);
27480
+ }
27481
+ return {
27482
+ roots,
27483
+ children,
27484
+ depth,
27485
+ by_id
27486
+ };
27487
+ }
27488
+ /************************************************************
27489
+ * The tidy tree, left to right.
27490
+ *
27491
+ * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
27492
+ * positions a node.
27493
+ ************************************************************/
27494
+ function layout_tree(nodes, edges, opts) {
27495
+ let o = Object.assign({}, DEFAULTS, opts || {});
27496
+ let t = spanning_tree(nodes, edges);
27497
+ let pos = /* @__PURE__ */ new Map();
27498
+ let col_w = [];
27499
+ for (let [id, d] of t.depth) {
27500
+ let w = t.by_id.get(id).w;
27501
+ col_w[d] = Math.max(col_w[d] || 0, w);
27502
+ }
27503
+ let col_x = [];
27504
+ let x = 0;
27505
+ for (let d = 0; d < col_w.length; d++) {
27506
+ col_x[d] = x;
27507
+ x += (col_w[d] || 0) + o.ranksep;
27508
+ }
27509
+ let height = /* @__PURE__ */ new Map();
27510
+ let measure = (id) => {
27511
+ let kids = t.children.get(id) || [];
27512
+ let own = t.by_id.get(id).h;
27513
+ if (!kids.length) {
27514
+ height.set(id, own);
27515
+ return own;
27516
+ }
27517
+ let sum = 0;
27518
+ for (let i = 0; i < kids.length; i++) {
27519
+ sum += measure(kids[i]);
27520
+ if (i > 0) sum += o.nodesep;
27521
+ }
27522
+ let h = Math.max(own, sum);
27523
+ height.set(id, h);
27524
+ return h;
27525
+ };
27526
+ for (let root of t.roots) measure(root);
27527
+ let place = (id, top) => {
27528
+ let n = t.by_id.get(id);
27529
+ let d = t.depth.get(id);
27530
+ let kids = t.children.get(id) || [];
27531
+ let block = height.get(id);
27532
+ let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
27533
+ if (!kids.length) {
27534
+ pos.set(id, {
27535
+ x: cx,
27536
+ y: top + block / 2
27537
+ });
27538
+ return;
27539
+ }
27540
+ let kids_h = 0;
27541
+ for (let i = 0; i < kids.length; i++) {
27542
+ kids_h += height.get(kids[i]);
27543
+ if (i > 0) kids_h += o.nodesep;
27544
+ }
27545
+ let cursor = top + (block - kids_h) / 2;
27546
+ for (let kid of kids) {
27547
+ place(kid, cursor);
27548
+ cursor += height.get(kid) + o.nodesep;
27549
+ }
27550
+ pos.set(id, {
27551
+ x: cx,
27552
+ y: top + block / 2
27553
+ });
27554
+ };
27555
+ let top = 0;
27556
+ for (let root of t.roots) {
27557
+ place(root, top);
27558
+ top += height.get(root) + o.nodesep * 2;
27559
+ }
27560
+ return pos;
27561
+ }
27562
+ /************************************************************
27563
+ * The radial tree: the root in the middle, a ring per depth.
27564
+ *
27565
+ * G6's `radial` places by MDS on the graph distances and pushes
27566
+ * overlaps apart afterwards, with a ring radius it is TOLD; twenty
27567
+ * cards of 172px on a ring of radius 200 have 52px each and pile
27568
+ * up. This one is the radial form of the tidy tree above: every
27569
+ * subtree gets an angular SECTOR proportional to its leaves, a node
27570
+ * sits in the middle of its sector, and the radius of each ring is
27571
+ * the larger of "one step further out than the ring before" and
27572
+ * "long enough for its nodes to sit side by side" -- so a hall
27573
+ * with a hundred devices is a wide, sparse fan, and nothing
27574
+ * overlaps by construction. Several roots share the circle around
27575
+ * a virtual centre.
27576
+ *
27577
+ * ranksep the step between rings
27578
+ * nodesep the gap between neighbours on a ring
27579
+ ************************************************************/
27580
+ function layout_radial(nodes, edges, opts) {
27581
+ let o = Object.assign({}, DEFAULTS, opts || {});
27582
+ let t = spanning_tree(nodes, edges);
27583
+ let pos = /* @__PURE__ */ new Map();
27584
+ if (!t.roots.length) return pos;
27585
+ let leaves = /* @__PURE__ */ new Map();
27586
+ let count = (id) => {
27587
+ let kids = t.children.get(id) || [];
27588
+ if (!kids.length) {
27589
+ leaves.set(id, 1);
27590
+ return 1;
27591
+ }
27592
+ let n = 0;
27593
+ for (let kid of kids) n += count(kid);
27594
+ leaves.set(id, n);
27595
+ return n;
27596
+ };
27597
+ let total = 0;
27598
+ for (let root of t.roots) total += count(root);
27599
+ let single = t.roots.length === 1;
27600
+ let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
27601
+ let angle = /* @__PURE__ */ new Map();
27602
+ let rings = [];
27603
+ let sector = (id, from, span) => {
27604
+ let a = from + span / 2;
27605
+ angle.set(id, a);
27606
+ let ring = ring_of(id);
27607
+ if (!rings[ring]) rings[ring] = [];
27608
+ let n = t.by_id.get(id);
27609
+ rings[ring].push({
27610
+ id,
27611
+ angle: a,
27612
+ diag: Math.hypot(n.w, n.h)
27613
+ });
27614
+ let cursor = from;
27615
+ for (let kid of t.children.get(id) || []) {
27616
+ let part = span * leaves.get(kid) / leaves.get(id);
27617
+ sector(kid, cursor, part);
27618
+ cursor += part;
27619
+ }
27620
+ };
27621
+ if (single) {
27622
+ let root = t.roots[0];
27623
+ angle.set(root, 0);
27624
+ let cursor = -Math.PI / 2;
27625
+ for (let kid of t.children.get(root) || []) {
27626
+ let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
27627
+ sector(kid, cursor, part);
27628
+ cursor += part;
27629
+ }
27630
+ } else {
27631
+ let cursor = -Math.PI / 2;
27632
+ for (let root of t.roots) {
27633
+ let part = 2 * Math.PI * leaves.get(root) / total;
27634
+ sector(root, cursor, part);
27635
+ cursor += part;
27636
+ }
27637
+ }
27638
+ let radius = [0];
27639
+ for (let ring = 1; ring < rings.length; ring++) {
27640
+ let r = radius[ring - 1] + o.ranksep;
27641
+ let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
27642
+ for (let i = 0; i < list.length && list.length > 1; i++) {
27643
+ let a = list[i];
27644
+ let b = list[(i + 1) % list.length];
27645
+ let gap = i + 1 < list.length ? b.angle - a.angle : b.angle + 2 * Math.PI - a.angle;
27646
+ if (gap > 1e-9) r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
27647
+ }
27648
+ radius[ring] = r;
27649
+ }
27650
+ for (let [id, a] of angle) {
27651
+ let r = radius[ring_of(id)] || 0;
27652
+ pos.set(id, {
27653
+ x: r * Math.cos(a),
27654
+ y: r * Math.sin(a)
27655
+ });
27656
+ }
27657
+ return pos;
27658
+ }
27659
+ /************************************************************
27660
+ * The outline: one node per row, indented by depth, in the
27661
+ * pre-order of the spanning tree. Rows are left-aligned on
27662
+ * their indent, so a column of chips reads as a list.
27663
+ ************************************************************/
27664
+ function layout_outline(nodes, edges, opts) {
27665
+ let o = Object.assign({}, DEFAULTS, opts || {});
27666
+ let t = spanning_tree(nodes, edges);
27667
+ let pos = /* @__PURE__ */ new Map();
27668
+ let y = 0;
27669
+ let visit = (id) => {
27670
+ let n = t.by_id.get(id);
27671
+ let d = t.depth.get(id);
27672
+ pos.set(id, {
27673
+ x: d * o.indent + n.w / 2,
27674
+ y: y + n.h / 2
27675
+ });
27676
+ y += n.h + o.nodesep;
27677
+ for (let kid of t.children.get(id) || []) visit(kid);
27678
+ };
27679
+ for (let root of t.roots) {
27680
+ visit(root);
27681
+ y += o.nodesep;
27682
+ }
27683
+ return pos;
27684
+ }
27685
+ //#endregion
27312
27686
  //#region node_modules/@babel/runtime/helpers/esm/typeof.js
27313
27687
  function _typeof(o) {
27314
27688
  "@babel/helpers - typeof";
@@ -54129,6 +54503,16 @@ var SELECT_RING = "rgba(59,130,246,0.95)";
54129
54503
  ***************************************************************/
54130
54504
  var _layouts = {
54131
54505
  "manual": { type: "manual" },
54506
+ "treedb-tree": {
54507
+ type: "treedb-tree",
54508
+ nodesep: 18,
54509
+ ranksep: 110
54510
+ },
54511
+ "treedb-outline": {
54512
+ type: "treedb-outline",
54513
+ nodesep: 12,
54514
+ indent: 48
54515
+ },
54132
54516
  "dagre": {
54133
54517
  type: "dagre",
54134
54518
  rankdir: "LR",
@@ -54141,6 +54525,11 @@ var _layouts = {
54141
54525
  nodesep: 30,
54142
54526
  ranksep: 80
54143
54527
  },
54528
+ "radial": {
54529
+ type: "treedb-radial",
54530
+ nodesep: 24,
54531
+ ranksep: 180
54532
+ },
54144
54533
  "d3-force": {
54145
54534
  type: "d3-force",
54146
54535
  link: {
@@ -54420,6 +54809,9 @@ function register_layouts(gobj) {
54420
54809
  if (!_g6_extensions_registered) {
54421
54810
  _g6_extensions_registered = true;
54422
54811
  register(ExtensionCategory.LAYOUT, "manual", ManualLayout);
54812
+ register(ExtensionCategory.LAYOUT, "treedb-tree", TreedbTreeLayout);
54813
+ register(ExtensionCategory.LAYOUT, "treedb-outline", TreedbOutlineLayout);
54814
+ register(ExtensionCategory.LAYOUT, "treedb-radial", TreedbRadialLayout);
54423
54815
  register(ExtensionCategory.NODE, "light", LightNode);
54424
54816
  }
54425
54817
  }
@@ -55031,8 +55423,13 @@ function build_ports(gobj, desc) {
55031
55423
  * included, because the saved arrangements were made with the
55032
55424
  * ports on the top and bottom edges.
55033
55425
  ************************************************************/
55426
+ var LR_LAYOUTS = [
55427
+ "dagre",
55428
+ "treedb-tree",
55429
+ "treedb-outline"
55430
+ ];
55034
55431
  function layout_direction(gobj) {
55035
- return gobj.priv.layout === "dagre" ? "LR" : "TB";
55432
+ return str_in_list(LR_LAYOUTS, gobj.priv.layout) ? "LR" : "TB";
55036
55433
  }
55037
55434
  function edge_type_for(gobj) {
55038
55435
  return layout_direction(gobj) === "LR" ? "cubic-horizontal" : "cubic";
@@ -56032,6 +56429,73 @@ var ManualLayout = class extends BaseLayout {
56032
56429
  }
56033
56430
  };
56034
56431
  /************************************************************
56432
+ * Custom G6 layouts: TreedbTreeLayout / TreedbOutlineLayout.
56433
+ *
56434
+ * Thin adapters over treedb_layout.js: the nodes with their sizes,
56435
+ * the edges with the RANK of the hook they leave by (the position
56436
+ * of the source port in the parent's ports, which is the schema's
56437
+ * column order), and the positions back. A `+N` chip's edge has a
56438
+ * source port too, so the chip sits at the end of its hook's
56439
+ * children, where the page ends.
56440
+ ************************************************************/
56441
+ function treedb_layout_input(data) {
56442
+ const { nodes = [], edges = [] } = data;
56443
+ let ports_of = /* @__PURE__ */ new Map();
56444
+ return {
56445
+ nodes: nodes.map((node) => {
56446
+ let size = node.style && node.style.size || [172, 96];
56447
+ let w = is_array(size) ? size[0] : size;
56448
+ let h = is_array(size) ? size.length > 1 ? size[1] : size[0] : size;
56449
+ let ports = node.style && is_array(node.style.ports) ? node.style.ports : [];
56450
+ ports_of.set(node.id, ports.map((p) => p.key));
56451
+ return {
56452
+ id: node.id,
56453
+ w,
56454
+ h
56455
+ };
56456
+ }),
56457
+ edges: edges.map((edge) => {
56458
+ let port = edge.style ? edge.style.sourcePort : void 0;
56459
+ let keys = ports_of.get(edge.source) || [];
56460
+ let rank = port ? keys.indexOf(port) : -1;
56461
+ return {
56462
+ source: edge.source,
56463
+ target: edge.target,
56464
+ rank: rank >= 0 ? rank : 0
56465
+ };
56466
+ })
56467
+ };
56468
+ }
56469
+ function treedb_layout_output(pos) {
56470
+ let out = [];
56471
+ for (let [id, p] of pos) out.push({
56472
+ id,
56473
+ style: {
56474
+ x: p.x,
56475
+ y: p.y
56476
+ }
56477
+ });
56478
+ return { nodes: out };
56479
+ }
56480
+ var TreedbTreeLayout = class extends BaseLayout {
56481
+ async execute(data, options) {
56482
+ let input = treedb_layout_input(data);
56483
+ return treedb_layout_output(layout_tree(input.nodes, input.edges, options));
56484
+ }
56485
+ };
56486
+ var TreedbOutlineLayout = class extends BaseLayout {
56487
+ async execute(data, options) {
56488
+ let input = treedb_layout_input(data);
56489
+ return treedb_layout_output(layout_outline(input.nodes, input.edges, options));
56490
+ }
56491
+ };
56492
+ var TreedbRadialLayout = class extends BaseLayout {
56493
+ async execute(data, options) {
56494
+ let input = treedb_layout_input(data);
56495
+ return treedb_layout_output(layout_radial(input.nodes, input.edges, options));
56496
+ }
56497
+ };
56498
+ /************************************************************
56035
56499
  * Node selection and resize handles
56036
56500
  ************************************************************/
56037
56501
  var RESIZE_HANDLE_SIZE = 8;
@@ -57693,7 +58157,7 @@ function auto_layout(gobj) {
57693
58157
  if (priv._layout_asked) return false;
57694
58158
  if (priv._nodes_total > 0 && priv._nodes_placed * 2 > priv._nodes_total) return false;
57695
58159
  if (!priv.graph) return false;
57696
- let name = "dagre";
58160
+ let name = "treedb-tree";
57697
58161
  gobj_write_attr(gobj, "layout", name);
57698
58162
  try {
57699
58163
  priv.graph.setLayout(_layouts[name]);
@@ -60530,6 +60994,9 @@ function option_label(name) {
60530
60994
  case "writing": return t("writing");
60531
60995
  case "edition": return t("edition");
60532
60996
  case "manual": return t("manual");
60997
+ case "treedb-tree": return t("treedb-tree");
60998
+ case "treedb-outline": return t("treedb-outline");
60999
+ case "radial": return t("radial");
60533
61000
  case "dagre": return t("dagre");
60534
61001
  case "antv-dagre": return t("antv-dagre");
60535
61002
  case "d3-force": return t("d3-force");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.69",
3
+ "version": "7.23.71",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -124,6 +124,7 @@ import {
124
124
  fold_node_key,
125
125
  fold_split_group_key,
126
126
  } from "./treedb_fold_model.js";
127
+ import {layout_tree, layout_outline, layout_radial} from "./treedb_layout.js";
127
128
 
128
129
  import {
129
130
  BaseLayout,
@@ -260,6 +261,23 @@ const _layouts = {
260
261
  * cards edge to edge. `antv-dagre` below keeps the top-down reading
261
262
  * for whoever wants it -- the two are the same algorithm read two
262
263
  * ways, so the layout picker is also the direction picker. */
264
+ /* OURS, made for a treedb (treedb_layout.js): a tidy tree read
265
+ * left to right, and an outline of one row per node. Both take
266
+ * the spanning tree of what is on screen -- the roots, the first
267
+ * parent that reaches a node, the hooks in schema order, the
268
+ * records in their order -- so opening a hook moves nothing that
269
+ * is not under or beside it. The default for a treedb nobody has
270
+ * arranged (see auto_layout). */
271
+ "treedb-tree": {
272
+ type: 'treedb-tree',
273
+ nodesep: 18,
274
+ ranksep: 110,
275
+ },
276
+ "treedb-outline": {
277
+ type: 'treedb-outline',
278
+ nodesep: 12,
279
+ indent: 48,
280
+ },
263
281
  "dagre": {
264
282
  type: 'dagre',
265
283
  rankdir: 'LR',
@@ -272,6 +290,14 @@ const _layouts = {
272
290
  nodesep: 30,
273
291
  ranksep: 80,
274
292
  },
293
+ /* Ours too: the root in the middle, a ring per depth, every ring
294
+ * as long as its cards need. G6's `radial` was tried first and
295
+ * piled twenty cards of 172px on a ring of a fixed radius. */
296
+ "radial": {
297
+ type: 'treedb-radial',
298
+ nodesep: 24,
299
+ ranksep: 180,
300
+ },
275
301
  "d3-force": {
276
302
  type: 'd3-force',
277
303
  link: {
@@ -780,6 +806,9 @@ function register_layouts(gobj)
780
806
  if(!_g6_extensions_registered) {
781
807
  _g6_extensions_registered = true;
782
808
  register(ExtensionCategory.LAYOUT, 'manual', ManualLayout);
809
+ register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
810
+ register(ExtensionCategory.LAYOUT, 'treedb-outline', TreedbOutlineLayout);
811
+ register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
783
812
  register(ExtensionCategory.NODE, 'light', LightNode);
784
813
  }
785
814
  }
@@ -1765,9 +1794,11 @@ function build_ports(gobj, desc)
1765
1794
  * included, because the saved arrangements were made with the
1766
1795
  * ports on the top and bottom edges.
1767
1796
  ************************************************************/
1797
+ const LR_LAYOUTS = ["dagre", "treedb-tree", "treedb-outline"];
1798
+
1768
1799
  function layout_direction(gobj)
1769
1800
  {
1770
- return (gobj.priv.layout === "dagre")? "LR" : "TB";
1801
+ return str_in_list(LR_LAYOUTS, gobj.priv.layout)? "LR" : "TB";
1771
1802
  }
1772
1803
 
1773
1804
  function edge_type_for(gobj)
@@ -3304,6 +3335,73 @@ class ManualLayout extends BaseLayout
3304
3335
  }
3305
3336
  }
3306
3337
 
3338
+ /************************************************************
3339
+ * Custom G6 layouts: TreedbTreeLayout / TreedbOutlineLayout.
3340
+ *
3341
+ * Thin adapters over treedb_layout.js: the nodes with their sizes,
3342
+ * the edges with the RANK of the hook they leave by (the position
3343
+ * of the source port in the parent's ports, which is the schema's
3344
+ * column order), and the positions back. A `+N` chip's edge has a
3345
+ * source port too, so the chip sits at the end of its hook's
3346
+ * children, where the page ends.
3347
+ ************************************************************/
3348
+ function treedb_layout_input(data)
3349
+ {
3350
+ const {nodes = [], edges = []} = data;
3351
+
3352
+ let ports_of = new Map();
3353
+ let in_nodes = nodes.map((node) => {
3354
+ let size = (node.style && node.style.size) || [172, 96];
3355
+ let w = is_array(size)? size[0] : size;
3356
+ let h = is_array(size)? (size.length > 1? size[1] : size[0]) : size;
3357
+ let ports = (node.style && is_array(node.style.ports))? node.style.ports : [];
3358
+ ports_of.set(node.id, ports.map((p) => p.key));
3359
+ return {id: node.id, w: w, h: h};
3360
+ });
3361
+
3362
+ let in_edges = edges.map((edge) => {
3363
+ let port = edge.style? edge.style.sourcePort : undefined;
3364
+ let keys = ports_of.get(edge.source) || [];
3365
+ let rank = port? keys.indexOf(port) : -1;
3366
+ return {source: edge.source, target: edge.target, rank: (rank >= 0)? rank : 0};
3367
+ });
3368
+
3369
+ return {nodes: in_nodes, edges: in_edges};
3370
+ }
3371
+
3372
+ function treedb_layout_output(pos)
3373
+ {
3374
+ let out = [];
3375
+ for(let [id, p] of pos) {
3376
+ out.push({id: id, style: {x: p.x, y: p.y}});
3377
+ }
3378
+ return {nodes: out};
3379
+ }
3380
+
3381
+ class TreedbTreeLayout extends BaseLayout
3382
+ {
3383
+ async execute(data, options) {
3384
+ let input = treedb_layout_input(data);
3385
+ return treedb_layout_output(layout_tree(input.nodes, input.edges, options));
3386
+ }
3387
+ }
3388
+
3389
+ class TreedbOutlineLayout extends BaseLayout
3390
+ {
3391
+ async execute(data, options) {
3392
+ let input = treedb_layout_input(data);
3393
+ return treedb_layout_output(layout_outline(input.nodes, input.edges, options));
3394
+ }
3395
+ }
3396
+
3397
+ class TreedbRadialLayout extends BaseLayout
3398
+ {
3399
+ async execute(data, options) {
3400
+ let input = treedb_layout_input(data);
3401
+ return treedb_layout_output(layout_radial(input.nodes, input.edges, options));
3402
+ }
3403
+ }
3404
+
3307
3405
  /************************************************************
3308
3406
  * Node selection and resize handles
3309
3407
  ************************************************************/
@@ -5860,7 +5958,10 @@ function auto_layout(gobj)
5860
5958
  return false;
5861
5959
  }
5862
5960
 
5863
- let name = "dagre";
5961
+ /* Ours, not dagre: the tidy tree keeps the records in their order
5962
+ * and moves nothing on a fold that is not under it (see
5963
+ * treedb_layout.js). dagre stays in the picker. */
5964
+ let name = "treedb-tree";
5864
5965
  gobj_write_attr(gobj, "layout", name);
5865
5966
  try {
5866
5967
  priv.graph.setLayout(_layouts[name]);