@yuneta/gobj-ui 7.23.72 → 7.23.73

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
@@ -1882,6 +1882,35 @@ the middle, and **a toolbar left with a single group is centred**. An unknown
1882
1882
  name is reported, not silently dropped: a typo would otherwise remove the save
1883
1883
  button with no trace of why.
1884
1884
 
1885
+ ### `C_YUI_FORM` — which field is editable, and the one `writable` does not govern
1886
+
1887
+ A form opened to LOOK at a record has no editable field: that is a property of
1888
+ the opening (`readonly`), not of the schema, and it wins over everything. Under
1889
+ it the schema decides — a column needs `writable` — **with one exception, and
1890
+ it is the whole of linking: an fkey.**
1891
+
1892
+ `writable` governs the WRITE of a column's VALUE, and an fkey is not written,
1893
+ it is **linked**. An fkey is normally declared with no `writable` flag at all
1894
+ (`treedb_authzs`'s `users.roles` is `['fkey']`, and so is almost every fkey in
1895
+ the tree), so reading "not writable" as "not editable" takes away the only way
1896
+ to link a record to its parent — with `users.roles`, the only way to give a
1897
+ person a role. The other half of the same rule is in the topic view, where the
1898
+ record travels back: it sends "the writable cols, the fkeys (a link is edited
1899
+ by linking) and the pkey". The two halves have to agree; when they did not, the
1900
+ form disabled the control whose value the save was still waiting for.
1901
+
1902
+ The rule lives in `form_field_readonly.js`, pure and tested. A `file` column is
1903
+ an fkey too (`['fkey','file']`) and is deliberately NOT covered: it answers
1904
+ `type: "file"`, its bytes travel beside the record, and the SDK declares as
1905
+ legal a `file` column that only a load fills.
1906
+
1907
+ **And `readonly` is an attribute of a TEXT control and of nothing else.** A
1908
+ `<select>`, a checkbox and a radio accept it and let the reader change the
1909
+ value anyway; only `disabled` stops them, and tom-select has to be told through
1910
+ its API. So the form applies `disabled` to those three, and the record is still
1911
+ read from a disabled control — it is collected by reading each marked control,
1912
+ never by submitting the form.
1913
+
1885
1914
  ### Selecting rows in any table — `yui_table_select.js`
1886
1915
 
1887
1916
  Deleting twenty rows one confirmation at a time is not a workflow. Any view
@@ -27492,6 +27492,28 @@ function spanning_tree(nodes, edges) {
27492
27492
  };
27493
27493
  }
27494
27494
  /************************************************************
27495
+ * The ids of a spanning tree in PARENT-BEFORE-CHILD order.
27496
+ *
27497
+ * Every pass below used to be a recursive walk, which is the natural
27498
+ * way to write it and the one that dies on a deep tree: a
27499
+ * self-referent hook (a place inside a place inside a place) is as
27500
+ * deep as the data says, and the stack is not. Read this list
27501
+ * forwards for a pre-order pass and BACKWARDS for a post-order one
27502
+ * -- a child always sits after its parent, so reversed it always
27503
+ * sits before it, which is the whole of what post-order was for.
27504
+ ************************************************************/
27505
+ function walk_order(t) {
27506
+ let order = [];
27507
+ let stack = t.roots.slice().reverse();
27508
+ while (stack.length) {
27509
+ let id = stack.pop();
27510
+ order.push(id);
27511
+ let kids = t.children.get(id) || [];
27512
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27513
+ }
27514
+ return order;
27515
+ }
27516
+ /************************************************************
27495
27517
  * The tidy tree, left to right.
27496
27518
  *
27497
27519
  * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
@@ -27512,56 +27534,45 @@ function layout_tree(nodes, edges, opts) {
27512
27534
  col_x[d] = x;
27513
27535
  x += (col_w[d] || 0) + o.ranksep;
27514
27536
  }
27537
+ let order = walk_order(t);
27515
27538
  let height = /* @__PURE__ */ new Map();
27516
- let measure = (id) => {
27539
+ for (let i = order.length - 1; i >= 0; i--) {
27540
+ let id = order[i];
27517
27541
  let kids = t.children.get(id) || [];
27518
27542
  let own = t.by_id.get(id).h;
27519
- if (!kids.length) {
27520
- height.set(id, own);
27521
- return own;
27522
- }
27523
27543
  let sum = 0;
27524
- for (let i = 0; i < kids.length; i++) {
27525
- sum += measure(kids[i]);
27526
- if (i > 0) sum += o.nodesep;
27544
+ for (let k = 0; k < kids.length; k++) {
27545
+ sum += height.get(kids[k]);
27546
+ if (k > 0) sum += o.nodesep;
27527
27547
  }
27528
- let h = Math.max(own, sum);
27529
- height.set(id, h);
27530
- return h;
27531
- };
27532
- for (let root of t.roots) measure(root);
27533
- let place = (id, top) => {
27534
- let n = t.by_id.get(id);
27548
+ height.set(id, kids.length ? Math.max(own, sum) : own);
27549
+ }
27550
+ let tops = /* @__PURE__ */ new Map();
27551
+ let top = 0;
27552
+ for (let root of t.roots) {
27553
+ tops.set(root, top);
27554
+ top += height.get(root) + o.nodesep * 2;
27555
+ }
27556
+ for (let id of order) {
27535
27557
  let d = t.depth.get(id);
27536
27558
  let kids = t.children.get(id) || [];
27537
27559
  let block = height.get(id);
27538
- let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
27539
- if (!kids.length) {
27540
- pos.set(id, {
27541
- x: cx,
27542
- y: top + block / 2
27543
- });
27544
- return;
27545
- }
27560
+ let own_top = tops.get(id);
27561
+ pos.set(id, {
27562
+ x: col_x[d] + col_w[d] / 2,
27563
+ y: own_top + block / 2
27564
+ });
27565
+ if (!kids.length) continue;
27546
27566
  let kids_h = 0;
27547
- for (let i = 0; i < kids.length; i++) {
27548
- kids_h += height.get(kids[i]);
27549
- if (i > 0) kids_h += o.nodesep;
27567
+ for (let k = 0; k < kids.length; k++) {
27568
+ kids_h += height.get(kids[k]);
27569
+ if (k > 0) kids_h += o.nodesep;
27550
27570
  }
27551
- let cursor = top + (block - kids_h) / 2;
27571
+ let cursor = own_top + (block - kids_h) / 2;
27552
27572
  for (let kid of kids) {
27553
- place(kid, cursor);
27573
+ tops.set(kid, cursor);
27554
27574
  cursor += height.get(kid) + o.nodesep;
27555
27575
  }
27556
- pos.set(id, {
27557
- x: cx,
27558
- y: top + block / 2
27559
- });
27560
- };
27561
- let top = 0;
27562
- for (let root of t.roots) {
27563
- place(root, top);
27564
- top += height.get(root) + o.nodesep * 2;
27565
27576
  }
27566
27577
  return pos;
27567
27578
  }
@@ -27588,40 +27599,53 @@ function layout_radial(nodes, edges, opts) {
27588
27599
  let t = spanning_tree(nodes, edges);
27589
27600
  let pos = /* @__PURE__ */ new Map();
27590
27601
  if (!t.roots.length) return pos;
27602
+ let order = walk_order(t);
27591
27603
  let leaves = /* @__PURE__ */ new Map();
27592
- let count = (id) => {
27604
+ for (let i = order.length - 1; i >= 0; i--) {
27605
+ let id = order[i];
27593
27606
  let kids = t.children.get(id) || [];
27594
27607
  if (!kids.length) {
27595
27608
  leaves.set(id, 1);
27596
- return 1;
27609
+ continue;
27597
27610
  }
27598
27611
  let n = 0;
27599
- for (let kid of kids) n += count(kid);
27612
+ for (let kid of kids) n += leaves.get(kid);
27600
27613
  leaves.set(id, n);
27601
- return n;
27602
- };
27614
+ }
27603
27615
  let total = 0;
27604
- for (let root of t.roots) total += count(root);
27616
+ for (let root of t.roots) total += leaves.get(root);
27605
27617
  let single = t.roots.length === 1;
27606
27618
  let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
27607
27619
  let angle = /* @__PURE__ */ new Map();
27608
27620
  let rings = [];
27609
- let sector = (id, from, span) => {
27610
- let a = from + span / 2;
27611
- angle.set(id, a);
27612
- let ring = ring_of(id);
27613
- if (!rings[ring]) rings[ring] = [];
27614
- let n = t.by_id.get(id);
27615
- rings[ring].push({
27616
- id,
27617
- angle: a,
27618
- diag: Math.hypot(n.w, n.h)
27619
- });
27620
- let cursor = from;
27621
- for (let kid of t.children.get(id) || []) {
27622
- let part = span * leaves.get(kid) / leaves.get(id);
27623
- sector(kid, cursor, part);
27624
- cursor += part;
27621
+ let sector = (root_id, root_from, root_span) => {
27622
+ let stack = [{
27623
+ id: root_id,
27624
+ from: root_from,
27625
+ span: root_span
27626
+ }];
27627
+ while (stack.length) {
27628
+ let { id, from, span } = stack.pop();
27629
+ let a = from + span / 2;
27630
+ angle.set(id, a);
27631
+ let ring = ring_of(id);
27632
+ if (!rings[ring]) rings[ring] = [];
27633
+ let n = t.by_id.get(id);
27634
+ rings[ring].push({
27635
+ id,
27636
+ angle: a,
27637
+ diag: Math.hypot(n.w, n.h)
27638
+ });
27639
+ let cursor = from;
27640
+ for (let kid of t.children.get(id) || []) {
27641
+ let part = span * leaves.get(kid) / leaves.get(id);
27642
+ stack.push({
27643
+ id: kid,
27644
+ from: cursor,
27645
+ span: part
27646
+ });
27647
+ cursor += part;
27648
+ }
27625
27649
  }
27626
27650
  };
27627
27651
  if (single) {
@@ -27672,15 +27696,20 @@ function layout_outline(nodes, edges, opts) {
27672
27696
  let t = spanning_tree(nodes, edges);
27673
27697
  let pos = /* @__PURE__ */ new Map();
27674
27698
  let y = 0;
27675
- let visit = (id) => {
27676
- let n = t.by_id.get(id);
27677
- let d = t.depth.get(id);
27678
- pos.set(id, {
27679
- x: d * o.indent + n.w / 2,
27680
- y: y + n.h / 2
27681
- });
27682
- y += n.h + o.nodesep;
27683
- for (let kid of t.children.get(id) || []) visit(kid);
27699
+ let visit = (root_id) => {
27700
+ let stack = [root_id];
27701
+ while (stack.length) {
27702
+ let id = stack.pop();
27703
+ let n = t.by_id.get(id);
27704
+ let d = t.depth.get(id);
27705
+ pos.set(id, {
27706
+ x: d * o.indent + n.w / 2,
27707
+ y: y + n.h / 2
27708
+ });
27709
+ y += n.h + o.nodesep;
27710
+ let kids = t.children.get(id) || [];
27711
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27712
+ }
27684
27713
  };
27685
27714
  for (let root of t.roots) {
27686
27715
  visit(root);
@@ -62107,6 +62136,8 @@ function ac_set_operation_mode(gobj, event, kw, src) {
62107
62136
  * into the URL, so what you are looking at stays linkable.
62108
62137
  ************************************************************/
62109
62138
  var LEGEND_GLYPH_STYLE = "font-size:1.5em; line-height:1; color:inherit;";
62139
+ var LEGEND_NAME_STYLE = "font-size:1.2em; line-height:1.2;";
62140
+ var LEGEND_COUNT_STYLE = "margin-left:.4rem; font-size:1em;";
62110
62141
  function refresh_legend(gobj) {
62111
62142
  let priv = gobj.priv;
62112
62143
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
@@ -62132,19 +62163,19 @@ function refresh_legend(gobj) {
62132
62163
  "span",
62133
62164
  {
62134
62165
  class: "GRAPH_LEGEND_NAME",
62135
- style: hidden ? "text-decoration:line-through; opacity:.6;" : ""
62166
+ style: LEGEND_NAME_STYLE + (hidden ? " text-decoration:line-through; opacity:.6;" : "")
62136
62167
  },
62137
62168
  topic_name
62138
62169
  ], [
62139
62170
  "span",
62140
62171
  {
62141
62172
  class: "GRAPH_LEGEND_COUNT has-text-grey",
62142
- style: "margin-left:.35rem; font-size:.8em;"
62173
+ style: LEGEND_COUNT_STYLE
62143
62174
  },
62144
62175
  hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
62145
62176
  ]);
62146
62177
  let body_attrs = {
62147
- class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " is-primary is-light" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62178
+ class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " pressed_state" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62148
62179
  type: "button",
62149
62180
  style: "gap:0; flex:0 0 auto;",
62150
62181
  title: is_main ? (0, i18next.t)("main topic") : hidden ? (0, i18next.t)("show topic") : (0, i18next.t)("hide topic"),
@@ -62168,7 +62199,7 @@ function refresh_legend(gobj) {
62168
62199
  if (is_main && state.main_chosen) extras.push([
62169
62200
  "button",
62170
62201
  {
62171
- class: "GRAPH_LEGEND_STAR button is-small is-warning is-light",
62202
+ class: "GRAPH_LEGEND_STAR button is-small pressed_state",
62172
62203
  type: "button",
62173
62204
  style: "padding:0 .4rem;",
62174
62205
  title: (0, i18next.t)("main topic"),
@@ -62229,7 +62260,7 @@ function refresh_legend(gobj) {
62229
62260
  if (!hidden && entry.linked && entry.loose > 0) extras.push([
62230
62261
  "button",
62231
62262
  {
62232
- class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " is-warning is-light" : ""),
62263
+ class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " pressed_state" : ""),
62233
62264
  type: "button",
62234
62265
  style: "padding:0 .45rem;",
62235
62266
  title: (0, i18next.t)("loose records"),
@@ -62249,7 +62280,7 @@ function refresh_legend(gobj) {
62249
62280
  if (!hidden) extras.push([
62250
62281
  "button",
62251
62282
  {
62252
- class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " is-primary is-light" : ""),
62283
+ class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " pressed_state" : ""),
62253
62284
  type: "button",
62254
62285
  style: "padding:0 .4rem;",
62255
62286
  title: (0, i18next.t)("highlight topic"),
@@ -27487,6 +27487,28 @@ function spanning_tree(nodes, edges) {
27487
27487
  };
27488
27488
  }
27489
27489
  /************************************************************
27490
+ * The ids of a spanning tree in PARENT-BEFORE-CHILD order.
27491
+ *
27492
+ * Every pass below used to be a recursive walk, which is the natural
27493
+ * way to write it and the one that dies on a deep tree: a
27494
+ * self-referent hook (a place inside a place inside a place) is as
27495
+ * deep as the data says, and the stack is not. Read this list
27496
+ * forwards for a pre-order pass and BACKWARDS for a post-order one
27497
+ * -- a child always sits after its parent, so reversed it always
27498
+ * sits before it, which is the whole of what post-order was for.
27499
+ ************************************************************/
27500
+ function walk_order(t) {
27501
+ let order = [];
27502
+ let stack = t.roots.slice().reverse();
27503
+ while (stack.length) {
27504
+ let id = stack.pop();
27505
+ order.push(id);
27506
+ let kids = t.children.get(id) || [];
27507
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27508
+ }
27509
+ return order;
27510
+ }
27511
+ /************************************************************
27490
27512
  * The tidy tree, left to right.
27491
27513
  *
27492
27514
  * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
@@ -27507,56 +27529,45 @@ function layout_tree(nodes, edges, opts) {
27507
27529
  col_x[d] = x;
27508
27530
  x += (col_w[d] || 0) + o.ranksep;
27509
27531
  }
27532
+ let order = walk_order(t);
27510
27533
  let height = /* @__PURE__ */ new Map();
27511
- let measure = (id) => {
27534
+ for (let i = order.length - 1; i >= 0; i--) {
27535
+ let id = order[i];
27512
27536
  let kids = t.children.get(id) || [];
27513
27537
  let own = t.by_id.get(id).h;
27514
- if (!kids.length) {
27515
- height.set(id, own);
27516
- return own;
27517
- }
27518
27538
  let sum = 0;
27519
- for (let i = 0; i < kids.length; i++) {
27520
- sum += measure(kids[i]);
27521
- if (i > 0) sum += o.nodesep;
27539
+ for (let k = 0; k < kids.length; k++) {
27540
+ sum += height.get(kids[k]);
27541
+ if (k > 0) sum += o.nodesep;
27522
27542
  }
27523
- let h = Math.max(own, sum);
27524
- height.set(id, h);
27525
- return h;
27526
- };
27527
- for (let root of t.roots) measure(root);
27528
- let place = (id, top) => {
27529
- let n = t.by_id.get(id);
27543
+ height.set(id, kids.length ? Math.max(own, sum) : own);
27544
+ }
27545
+ let tops = /* @__PURE__ */ new Map();
27546
+ let top = 0;
27547
+ for (let root of t.roots) {
27548
+ tops.set(root, top);
27549
+ top += height.get(root) + o.nodesep * 2;
27550
+ }
27551
+ for (let id of order) {
27530
27552
  let d = t.depth.get(id);
27531
27553
  let kids = t.children.get(id) || [];
27532
27554
  let block = height.get(id);
27533
- let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
27534
- if (!kids.length) {
27535
- pos.set(id, {
27536
- x: cx,
27537
- y: top + block / 2
27538
- });
27539
- return;
27540
- }
27555
+ let own_top = tops.get(id);
27556
+ pos.set(id, {
27557
+ x: col_x[d] + col_w[d] / 2,
27558
+ y: own_top + block / 2
27559
+ });
27560
+ if (!kids.length) continue;
27541
27561
  let kids_h = 0;
27542
- for (let i = 0; i < kids.length; i++) {
27543
- kids_h += height.get(kids[i]);
27544
- if (i > 0) kids_h += o.nodesep;
27562
+ for (let k = 0; k < kids.length; k++) {
27563
+ kids_h += height.get(kids[k]);
27564
+ if (k > 0) kids_h += o.nodesep;
27545
27565
  }
27546
- let cursor = top + (block - kids_h) / 2;
27566
+ let cursor = own_top + (block - kids_h) / 2;
27547
27567
  for (let kid of kids) {
27548
- place(kid, cursor);
27568
+ tops.set(kid, cursor);
27549
27569
  cursor += height.get(kid) + o.nodesep;
27550
27570
  }
27551
- pos.set(id, {
27552
- x: cx,
27553
- y: top + block / 2
27554
- });
27555
- };
27556
- let top = 0;
27557
- for (let root of t.roots) {
27558
- place(root, top);
27559
- top += height.get(root) + o.nodesep * 2;
27560
27571
  }
27561
27572
  return pos;
27562
27573
  }
@@ -27583,40 +27594,53 @@ function layout_radial(nodes, edges, opts) {
27583
27594
  let t = spanning_tree(nodes, edges);
27584
27595
  let pos = /* @__PURE__ */ new Map();
27585
27596
  if (!t.roots.length) return pos;
27597
+ let order = walk_order(t);
27586
27598
  let leaves = /* @__PURE__ */ new Map();
27587
- let count = (id) => {
27599
+ for (let i = order.length - 1; i >= 0; i--) {
27600
+ let id = order[i];
27588
27601
  let kids = t.children.get(id) || [];
27589
27602
  if (!kids.length) {
27590
27603
  leaves.set(id, 1);
27591
- return 1;
27604
+ continue;
27592
27605
  }
27593
27606
  let n = 0;
27594
- for (let kid of kids) n += count(kid);
27607
+ for (let kid of kids) n += leaves.get(kid);
27595
27608
  leaves.set(id, n);
27596
- return n;
27597
- };
27609
+ }
27598
27610
  let total = 0;
27599
- for (let root of t.roots) total += count(root);
27611
+ for (let root of t.roots) total += leaves.get(root);
27600
27612
  let single = t.roots.length === 1;
27601
27613
  let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
27602
27614
  let angle = /* @__PURE__ */ new Map();
27603
27615
  let rings = [];
27604
- let sector = (id, from, span) => {
27605
- let a = from + span / 2;
27606
- angle.set(id, a);
27607
- let ring = ring_of(id);
27608
- if (!rings[ring]) rings[ring] = [];
27609
- let n = t.by_id.get(id);
27610
- rings[ring].push({
27611
- id,
27612
- angle: a,
27613
- diag: Math.hypot(n.w, n.h)
27614
- });
27615
- let cursor = from;
27616
- for (let kid of t.children.get(id) || []) {
27617
- let part = span * leaves.get(kid) / leaves.get(id);
27618
- sector(kid, cursor, part);
27619
- cursor += part;
27616
+ let sector = (root_id, root_from, root_span) => {
27617
+ let stack = [{
27618
+ id: root_id,
27619
+ from: root_from,
27620
+ span: root_span
27621
+ }];
27622
+ while (stack.length) {
27623
+ let { id, from, span } = stack.pop();
27624
+ let a = from + span / 2;
27625
+ angle.set(id, a);
27626
+ let ring = ring_of(id);
27627
+ if (!rings[ring]) rings[ring] = [];
27628
+ let n = t.by_id.get(id);
27629
+ rings[ring].push({
27630
+ id,
27631
+ angle: a,
27632
+ diag: Math.hypot(n.w, n.h)
27633
+ });
27634
+ let cursor = from;
27635
+ for (let kid of t.children.get(id) || []) {
27636
+ let part = span * leaves.get(kid) / leaves.get(id);
27637
+ stack.push({
27638
+ id: kid,
27639
+ from: cursor,
27640
+ span: part
27641
+ });
27642
+ cursor += part;
27643
+ }
27620
27644
  }
27621
27645
  };
27622
27646
  if (single) {
@@ -27667,15 +27691,20 @@ function layout_outline(nodes, edges, opts) {
27667
27691
  let t = spanning_tree(nodes, edges);
27668
27692
  let pos = /* @__PURE__ */ new Map();
27669
27693
  let y = 0;
27670
- let visit = (id) => {
27671
- let n = t.by_id.get(id);
27672
- let d = t.depth.get(id);
27673
- pos.set(id, {
27674
- x: d * o.indent + n.w / 2,
27675
- y: y + n.h / 2
27676
- });
27677
- y += n.h + o.nodesep;
27678
- for (let kid of t.children.get(id) || []) visit(kid);
27694
+ let visit = (root_id) => {
27695
+ let stack = [root_id];
27696
+ while (stack.length) {
27697
+ let id = stack.pop();
27698
+ let n = t.by_id.get(id);
27699
+ let d = t.depth.get(id);
27700
+ pos.set(id, {
27701
+ x: d * o.indent + n.w / 2,
27702
+ y: y + n.h / 2
27703
+ });
27704
+ y += n.h + o.nodesep;
27705
+ let kids = t.children.get(id) || [];
27706
+ for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
27707
+ }
27679
27708
  };
27680
27709
  for (let root of t.roots) {
27681
27710
  visit(root);
@@ -62102,6 +62131,8 @@ function ac_set_operation_mode(gobj, event, kw, src) {
62102
62131
  * into the URL, so what you are looking at stays linkable.
62103
62132
  ************************************************************/
62104
62133
  var LEGEND_GLYPH_STYLE = "font-size:1.5em; line-height:1; color:inherit;";
62134
+ var LEGEND_NAME_STYLE = "font-size:1.2em; line-height:1.2;";
62135
+ var LEGEND_COUNT_STYLE = "margin-left:.4rem; font-size:1em;";
62105
62136
  function refresh_legend(gobj) {
62106
62137
  let priv = gobj.priv;
62107
62138
  let $container = gobj_read_attr(gobj, "$container");
@@ -62127,19 +62158,19 @@ function refresh_legend(gobj) {
62127
62158
  "span",
62128
62159
  {
62129
62160
  class: "GRAPH_LEGEND_NAME",
62130
- style: hidden ? "text-decoration:line-through; opacity:.6;" : ""
62161
+ style: LEGEND_NAME_STYLE + (hidden ? " text-decoration:line-through; opacity:.6;" : "")
62131
62162
  },
62132
62163
  topic_name
62133
62164
  ], [
62134
62165
  "span",
62135
62166
  {
62136
62167
  class: "GRAPH_LEGEND_COUNT has-text-grey",
62137
- style: "margin-left:.35rem; font-size:.8em;"
62168
+ style: LEGEND_COUNT_STYLE
62138
62169
  },
62139
62170
  hidden ? `${entry.total}` : `${entry.visible}/${entry.total}`
62140
62171
  ]);
62141
62172
  let body_attrs = {
62142
- class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " is-primary is-light" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62173
+ class: "GRAPH_LEGEND_ITEM button is-small" + (focused ? " pressed_state" : "") + (is_main ? " GRAPH_LEGEND_MAIN" : ""),
62143
62174
  type: "button",
62144
62175
  style: "gap:0; flex:0 0 auto;",
62145
62176
  title: is_main ? t("main topic") : hidden ? t("show topic") : t("hide topic"),
@@ -62163,7 +62194,7 @@ function refresh_legend(gobj) {
62163
62194
  if (is_main && state.main_chosen) extras.push([
62164
62195
  "button",
62165
62196
  {
62166
- class: "GRAPH_LEGEND_STAR button is-small is-warning is-light",
62197
+ class: "GRAPH_LEGEND_STAR button is-small pressed_state",
62167
62198
  type: "button",
62168
62199
  style: "padding:0 .4rem;",
62169
62200
  title: t("main topic"),
@@ -62224,7 +62255,7 @@ function refresh_legend(gobj) {
62224
62255
  if (!hidden && entry.linked && entry.loose > 0) extras.push([
62225
62256
  "button",
62226
62257
  {
62227
- class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " is-warning is-light" : ""),
62258
+ class: "GRAPH_LEGEND_LOOSE button is-small" + (entry.loose_shown ? " pressed_state" : ""),
62228
62259
  type: "button",
62229
62260
  style: "padding:0 .45rem;",
62230
62261
  title: t("loose records"),
@@ -62244,7 +62275,7 @@ function refresh_legend(gobj) {
62244
62275
  if (!hidden) extras.push([
62245
62276
  "button",
62246
62277
  {
62247
- class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " is-primary is-light" : ""),
62278
+ class: "GRAPH_LEGEND_FOCUS button is-small" + (focused ? " pressed_state" : ""),
62248
62279
  type: "button",
62249
62280
  style: "padding:0 .4rem;",
62250
62281
  title: t("highlight topic"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.72",
3
+ "version": "7.23.73",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -2339,6 +2339,15 @@ function ac_set_operation_mode(gobj, event, kw, src)
2339
2339
  * stays small so the strip keeps its height; the glyph inside grows. */
2340
2340
  const LEGEND_GLYPH_STYLE = 'font-size:1.5em; line-height:1; color:inherit;';
2341
2341
 
2342
+ /* And the same for what a chip SAYS. The legend is a layer control --
2343
+ * a gadget somebody uses, not a footnote somebody glances at -- and a
2344
+ * topic name is the thing it is read for, so 0.75rem is the wrong
2345
+ * size for it. The button stays `is-small` because the strip has to
2346
+ * hold a chip per topic on one row; the name and the count come back
2347
+ * up inside it, exactly as the glyphs did. */
2348
+ const LEGEND_NAME_STYLE = 'font-size:1.2em; line-height:1.2;';
2349
+ const LEGEND_COUNT_STYLE = 'margin-left:.4rem; font-size:1em;';
2350
+
2342
2351
  function refresh_legend(gobj)
2343
2352
  {
2344
2353
  let priv = gobj.priv;
@@ -2389,10 +2398,11 @@ function refresh_legend(gobj)
2389
2398
  `border:1px solid rgba(0,0,0,.25);` +
2390
2399
  (hidden? 'opacity:.35;' : '')}],
2391
2400
  ['span', {class: 'GRAPH_LEGEND_NAME',
2392
- style: hidden? 'text-decoration:line-through; opacity:.6;' : ''},
2401
+ style: LEGEND_NAME_STYLE +
2402
+ (hidden? ' text-decoration:line-through; opacity:.6;' : '')},
2393
2403
  topic_name],
2394
2404
  ['span', {class: 'GRAPH_LEGEND_COUNT has-text-grey',
2395
- style: 'margin-left:.35rem; font-size:.8em;'},
2405
+ style: LEGEND_COUNT_STYLE},
2396
2406
  hidden? `${entry.total}` : `${entry.visible}/${entry.total}`]
2397
2407
  );
2398
2408
 
@@ -2402,7 +2412,7 @@ function refresh_legend(gobj)
2402
2412
  * every chip was dead except its star. */
2403
2413
  let body_attrs = {
2404
2414
  class: 'GRAPH_LEGEND_ITEM button is-small' +
2405
- (focused? ' is-primary is-light' : '') +
2415
+ (focused? ' pressed_state' : '') +
2406
2416
  (is_main? ' GRAPH_LEGEND_MAIN' : ''),
2407
2417
  type: 'button',
2408
2418
  style: 'gap:0; flex:0 0 auto;',
@@ -2430,7 +2440,7 @@ function refresh_legend(gobj)
2430
2440
  * is a button too: pressing it hands the choice back to the
2431
2441
  * graph (deduced again). On a deduced one it is a mark. */
2432
2442
  if(is_main && state.main_chosen) {
2433
- extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small is-warning is-light',
2443
+ extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small pressed_state',
2434
2444
  type: 'button', style: 'padding:0 .4rem;',
2435
2445
  title: t('main topic'), 'data-i18n-title': 'main topic',
2436
2446
  'aria-label': t('main topic'), 'aria-pressed': 'true'},
@@ -2466,7 +2476,7 @@ function refresh_legend(gobj)
2466
2476
  if(!hidden && entry.linked && entry.loose > 0) {
2467
2477
  extras.push(['button', {
2468
2478
  class: 'GRAPH_LEGEND_LOOSE button is-small' +
2469
- (entry.loose_shown? ' is-warning is-light' : ''),
2479
+ (entry.loose_shown? ' pressed_state' : ''),
2470
2480
  type: 'button', style: 'padding:0 .45rem;',
2471
2481
  title: t('loose records'), 'data-i18n-title': 'loose records',
2472
2482
  'aria-label': t('loose records'),
@@ -2483,7 +2493,7 @@ function refresh_legend(gobj)
2483
2493
  if(!hidden) {
2484
2494
  extras.push(['button', {
2485
2495
  class: 'GRAPH_LEGEND_FOCUS button is-small' +
2486
- (focused? ' is-primary is-light' : ''),
2496
+ (focused? ' pressed_state' : ''),
2487
2497
  type: 'button', style: 'padding:0 .4rem;',
2488
2498
  title: t('highlight topic'), 'data-i18n-title': 'highlight topic',
2489
2499
  'aria-label': t('highlight topic'),
@@ -142,6 +142,32 @@ export function spanning_tree(nodes, edges)
142
142
  return {roots: roots, children: children, depth: depth, by_id: by_id};
143
143
  }
144
144
 
145
+ /************************************************************
146
+ * The ids of a spanning tree in PARENT-BEFORE-CHILD order.
147
+ *
148
+ * Every pass below used to be a recursive walk, which is the natural
149
+ * way to write it and the one that dies on a deep tree: a
150
+ * self-referent hook (a place inside a place inside a place) is as
151
+ * deep as the data says, and the stack is not. Read this list
152
+ * forwards for a pre-order pass and BACKWARDS for a post-order one
153
+ * -- a child always sits after its parent, so reversed it always
154
+ * sits before it, which is the whole of what post-order was for.
155
+ ************************************************************/
156
+ function walk_order(t)
157
+ {
158
+ let order = [];
159
+ let stack = t.roots.slice().reverse();
160
+ while(stack.length) {
161
+ let id = stack.pop();
162
+ order.push(id);
163
+ let kids = t.children.get(id) || [];
164
+ for(let i = kids.length - 1; i >= 0; i--) {
165
+ stack.push(kids[i]);
166
+ }
167
+ }
168
+ return order;
169
+ }
170
+
145
171
  /************************************************************
146
172
  * The tidy tree, left to right.
147
173
  *
@@ -167,60 +193,54 @@ export function layout_tree(nodes, edges, opts)
167
193
  x += (col_w[d] || 0) + o.ranksep;
168
194
  }
169
195
 
170
- /* Subtree heights, post-order. */
196
+ /* Subtree heights: post-order, so children before parents. */
197
+ let order = walk_order(t);
171
198
  let height = new Map();
172
- let measure = (id) => {
199
+ for(let i = order.length - 1; i >= 0; i--) {
200
+ let id = order[i];
173
201
  let kids = t.children.get(id) || [];
174
202
  let own = t.by_id.get(id).h;
175
- if(!kids.length) {
176
- height.set(id, own);
177
- return own;
178
- }
179
203
  let sum = 0;
180
- for(let i = 0; i < kids.length; i++) {
181
- sum += measure(kids[i]);
182
- if(i > 0) {
204
+ for(let k = 0; k < kids.length; k++) {
205
+ sum += height.get(kids[k]);
206
+ if(k > 0) {
183
207
  sum += o.nodesep;
184
208
  }
185
209
  }
186
- let h = Math.max(own, sum);
187
- height.set(id, h);
188
- return h;
189
- };
190
- for(let root of t.roots) {
191
- measure(root);
210
+ height.set(id, kids.length? Math.max(own, sum) : own);
192
211
  }
193
212
 
194
213
  /* Placement, pre-order: a node is centred on the block of its
195
- * children, which starts at the top of its own block. */
196
- let place = (id, top) => {
197
- let n = t.by_id.get(id);
214
+ * children, which starts at the top of its own block. Every node
215
+ * in a column is centred on the column, whatever its own width --
216
+ * the cards of one depth line up by their middles. */
217
+ let tops = new Map();
218
+ let top = 0;
219
+ for(let root of t.roots) {
220
+ tops.set(root, top);
221
+ top += height.get(root) + o.nodesep * 2;
222
+ }
223
+ for(let id of order) {
198
224
  let d = t.depth.get(id);
199
225
  let kids = t.children.get(id) || [];
200
226
  let block = height.get(id);
201
- let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
227
+ let own_top = tops.get(id);
228
+ pos.set(id, {x: col_x[d] + col_w[d] / 2, y: own_top + block / 2});
202
229
  if(!kids.length) {
203
- pos.set(id, {x: cx, y: top + block / 2});
204
- return;
230
+ continue;
205
231
  }
206
232
  let kids_h = 0;
207
- for(let i = 0; i < kids.length; i++) {
208
- kids_h += height.get(kids[i]);
209
- if(i > 0) {
233
+ for(let k = 0; k < kids.length; k++) {
234
+ kids_h += height.get(kids[k]);
235
+ if(k > 0) {
210
236
  kids_h += o.nodesep;
211
237
  }
212
238
  }
213
- let cursor = top + (block - kids_h) / 2;
239
+ let cursor = own_top + (block - kids_h) / 2;
214
240
  for(let kid of kids) {
215
- place(kid, cursor);
241
+ tops.set(kid, cursor);
216
242
  cursor += height.get(kid) + o.nodesep;
217
243
  }
218
- pos.set(id, {x: cx, y: top + block / 2});
219
- };
220
- let top = 0;
221
- for(let root of t.roots) {
222
- place(root, top);
223
- top += height.get(root) + o.nodesep * 2;
224
244
  }
225
245
 
226
246
  return pos;
@@ -253,24 +273,25 @@ export function layout_radial(nodes, edges, opts)
253
273
  return pos;
254
274
  }
255
275
 
256
- /* Leaves under each node: the sector it deserves. */
276
+ /* Leaves under each node: the sector it deserves. Post-order. */
277
+ let order = walk_order(t);
257
278
  let leaves = new Map();
258
- let count = (id) => {
279
+ for(let i = order.length - 1; i >= 0; i--) {
280
+ let id = order[i];
259
281
  let kids = t.children.get(id) || [];
260
282
  if(!kids.length) {
261
283
  leaves.set(id, 1);
262
- return 1;
284
+ continue;
263
285
  }
264
286
  let n = 0;
265
287
  for(let kid of kids) {
266
- n += count(kid);
288
+ n += leaves.get(kid);
267
289
  }
268
290
  leaves.set(id, n);
269
- return n;
270
- };
291
+ }
271
292
  let total = 0;
272
293
  for(let root of t.roots) {
273
- total += count(root);
294
+ total += leaves.get(root);
274
295
  }
275
296
 
276
297
  /* One root sits at the centre; several sit on the first ring
@@ -283,20 +304,27 @@ export function layout_radial(nodes, edges, opts)
283
304
  * Angles do not depend on the radii, so they come first. */
284
305
  let angle = new Map();
285
306
  let rings = []; /* ring -> [{id, angle, diag}] */
286
- let sector = (id, from, span) => {
287
- let a = from + span / 2;
288
- angle.set(id, a);
289
- let ring = ring_of(id);
290
- if(!rings[ring]) {
291
- rings[ring] = [];
292
- }
293
- let n = t.by_id.get(id);
294
- rings[ring].push({id: id, angle: a, diag: Math.hypot(n.w, n.h)});
295
- let cursor = from;
296
- for(let kid of (t.children.get(id) || [])) {
297
- let part = span * leaves.get(kid) / leaves.get(id);
298
- sector(kid, cursor, part);
299
- cursor += part;
307
+ /* An explicit stack, not recursion: the sectors of a ring are
308
+ * sorted by angle in pass 2, so the order they are pushed in does
309
+ * not matter -- only that every node gets its own. */
310
+ let sector = (root_id, root_from, root_span) => {
311
+ let stack = [{id: root_id, from: root_from, span: root_span}];
312
+ while(stack.length) {
313
+ let {id, from, span} = stack.pop();
314
+ let a = from + span / 2;
315
+ angle.set(id, a);
316
+ let ring = ring_of(id);
317
+ if(!rings[ring]) {
318
+ rings[ring] = [];
319
+ }
320
+ let n = t.by_id.get(id);
321
+ rings[ring].push({id: id, angle: a, diag: Math.hypot(n.w, n.h)});
322
+ let cursor = from;
323
+ for(let kid of (t.children.get(id) || [])) {
324
+ let part = span * leaves.get(kid) / leaves.get(id);
325
+ stack.push({id: kid, from: cursor, span: part});
326
+ cursor += part;
327
+ }
300
328
  }
301
329
  };
302
330
  if(single) {
@@ -362,13 +390,20 @@ export function layout_outline(nodes, edges, opts)
362
390
  let pos = new Map();
363
391
 
364
392
  let y = 0;
365
- let visit = (id) => {
366
- let n = t.by_id.get(id);
367
- let d = t.depth.get(id);
368
- pos.set(id, {x: d * o.indent + n.w / 2, y: y + n.h / 2});
369
- y += n.h + o.nodesep;
370
- for(let kid of (t.children.get(id) || [])) {
371
- visit(kid);
393
+ let visit = (root_id) => {
394
+ let stack = [root_id];
395
+ while(stack.length) {
396
+ let id = stack.pop();
397
+ let n = t.by_id.get(id);
398
+ let d = t.depth.get(id);
399
+ pos.set(id, {x: d * o.indent + n.w / 2, y: y + n.h / 2});
400
+ y += n.h + o.nodesep;
401
+ /* Reversed, so a sibling pops in the order it was given:
402
+ * here the order of the walk IS the order of the rows. */
403
+ let kids = t.children.get(id) || [];
404
+ for(let i = kids.length - 1; i >= 0; i--) {
405
+ stack.push(kids[i]);
406
+ }
372
407
  }
373
408
  };
374
409
  for(let root of t.roots) {
@@ -170,3 +170,35 @@ describe("the outline", () => {
170
170
  expect(pos.get("norte").y).toBe(96 + 10 + 48);
171
171
  });
172
172
  });
173
+
174
+ describe("a deep tree", () => {
175
+ /* A self-referent hook -- a place inside a place inside a place --
176
+ * is as deep as the data says. The walks are iterative for exactly
177
+ * this: recursive ones died here with "Maximum call stack size
178
+ * exceeded", and a graph that cannot be drawn is not a layout
179
+ * choice, it is an exception in the console. */
180
+ const DEEP = 20000;
181
+ const deep_nodes = [];
182
+ const deep_edges = [];
183
+ for(let i = 0; i < DEEP; i++) {
184
+ deep_nodes.push({id: `n${i}`, w: 100, h: 40});
185
+ if(i > 0) {
186
+ deep_edges.push({source: `n${i - 1}`, target: `n${i}`, rank: 0});
187
+ }
188
+ }
189
+
190
+ test("the spanning tree reaches the bottom", () => {
191
+ let t = spanning_tree(deep_nodes, deep_edges);
192
+ expect(t.roots).toEqual(["n0"]);
193
+ expect(t.depth.get(`n${DEEP - 1}`)).toBe(DEEP - 1);
194
+ });
195
+
196
+ test("the three layouts place every node of it", () => {
197
+ for(let layout of [layout_tree, layout_outline, layout_radial]) {
198
+ let pos = layout(deep_nodes, deep_edges);
199
+ expect(pos.size).toBe(DEEP);
200
+ expect(Number.isFinite(pos.get(`n${DEEP - 1}`).x)).toBe(true);
201
+ expect(Number.isFinite(pos.get(`n${DEEP - 1}`).y)).toBe(true);
202
+ }
203
+ });
204
+ });