@yuneta/gobj-ui 7.23.83 → 7.23.85

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.
@@ -173,6 +173,7 @@ let PRIVATE_DATA = {
173
173
  layout: null,
174
174
  _topics_subscribed: {},
175
175
  _legend_state: null, /* the engine's last EV_LEGEND_STATE */
176
+ _find_result: null, /* ...and its last EV_FIND_RESULT */
176
177
  _links_subscribed: false, /* EV_TREEDB_NODE_LINKED/UNLINKED (treedb-wide) */
177
178
 
178
179
  is_pinhold_window: false, // inherited of v6, todo review
@@ -303,6 +304,18 @@ function mt_start(gobj)
303
304
  {
304
305
  let priv = gobj.priv;
305
306
 
307
+ /* The legend is built from the engine's EV_LEGEND_STATE, which
308
+ * arrives after a reconcile and not when the language changes:
309
+ * a chip's title says an ACTION (show / hide this topic), and
310
+ * the strip would keep the old language until somebody folded
311
+ * something. The keys travel in `data-i18n-title` too, so this
312
+ * is the belt to that pair of braces -- and it is what repaints
313
+ * the counted line of the find box, which has no key at all. */
314
+ let shell = yui_shell_of(gobj);
315
+ if(shell) {
316
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
317
+ }
318
+
306
319
  if(priv.gobj_nodes_tree) {
307
320
  gobj_start(priv.gobj_nodes_tree);
308
321
  }
@@ -323,6 +336,7 @@ function mt_stop(gobj)
323
336
  let shell = yui_shell_of(gobj);
324
337
  if(shell) {
325
338
  yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
339
+ gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
326
340
  }
327
341
  close_json_viewer(gobj);
328
342
  gobj_stop_children(gobj);
@@ -2523,6 +2537,8 @@ function refresh_legend(gobj)
2523
2537
  hidden? `${entry.total}` : `${entry.visible}/${entry.total}`]
2524
2538
  );
2525
2539
 
2540
+ let body_key = is_main? 'main topic' : (hidden? 'show topic' : 'hide topic');
2541
+
2526
2542
  /* Built as an object first: an attribute that is `undefined`
2527
2543
  * is still SET by createElement2 (to the string "undefined"),
2528
2544
  * and a `disabled="undefined"` button is a disabled button --
@@ -2533,7 +2549,14 @@ function refresh_legend(gobj)
2533
2549
  (is_main? ' GRAPH_LEGEND_MAIN' : ''),
2534
2550
  type: 'button',
2535
2551
  style: 'gap:0; flex:0 0 auto;' + (focused? ' ' + LEGEND_FOCUSED_STYLE : ''),
2536
- title: is_main? t('main topic') : (hidden? t('show topic') : t('hide topic')),
2552
+ /* The KEY as well as the text: the strip is redrawn only
2553
+ * when the engine speaks again (EV_LEGEND_STATE), and a
2554
+ * language change is not that -- refresh_language() reaches
2555
+ * what carries `data-i18n-title` and nothing else. The key
2556
+ * is picked here because the chip is rebuilt whenever the
2557
+ * state behind it changes. */
2558
+ title: t(body_key),
2559
+ 'data-i18n-title': body_key,
2537
2560
  'aria-label': topic_name,
2538
2561
  'aria-pressed': hidden? 'false' : 'true',
2539
2562
  };
@@ -2560,7 +2583,9 @@ function refresh_legend(gobj)
2560
2583
  extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small pressed_state',
2561
2584
  type: 'button', style: 'padding:0 .4rem;',
2562
2585
  title: t('main topic'), 'data-i18n-title': 'main topic',
2563
- 'aria-label': t('main topic'), 'aria-pressed': 'true'},
2586
+ 'aria-label': t('main topic'),
2587
+ 'data-i18n-aria-label': 'main topic',
2588
+ 'aria-pressed': 'true'},
2564
2589
  [['span', {style: LEGEND_GLYPH_STYLE}, '★']], {
2565
2590
  click: (evt) => {
2566
2591
  evt.stopPropagation();
@@ -2577,7 +2602,8 @@ function refresh_legend(gobj)
2577
2602
  extras.push(['button', {class: 'GRAPH_LEGEND_STAR button is-small',
2578
2603
  type: 'button', style: 'padding:0 .4rem;',
2579
2604
  title: t('main topic'), 'data-i18n-title': 'main topic',
2580
- 'aria-label': t('main topic')},
2605
+ 'aria-label': t('main topic'),
2606
+ 'data-i18n-aria-label': 'main topic'},
2581
2607
  [['span', {style: LEGEND_GLYPH_STYLE}, '☆']], {
2582
2608
  click: (evt) => {
2583
2609
  evt.stopPropagation();
@@ -2597,6 +2623,7 @@ function refresh_legend(gobj)
2597
2623
  type: 'button', style: 'padding:0 .45rem;',
2598
2624
  title: t('loose records'), 'data-i18n-title': 'loose records',
2599
2625
  'aria-label': t('loose records'),
2626
+ 'data-i18n-aria-label': 'loose records',
2600
2627
  'aria-pressed': entry.loose_shown? 'true' : 'false'
2601
2628
  }, `+${entry.loose}`, {
2602
2629
  click: (evt) => {
@@ -2614,6 +2641,7 @@ function refresh_legend(gobj)
2614
2641
  type: 'button', style: 'padding:0 .4rem;',
2615
2642
  title: t('highlight topic'), 'data-i18n-title': 'highlight topic',
2616
2643
  'aria-label': t('highlight topic'),
2644
+ 'data-i18n-aria-label': 'highlight topic',
2617
2645
  'aria-pressed': focused? 'true' : 'false'
2618
2646
  }, [['i', {class: 'yi-location-crosshairs', style: LEGEND_GLYPH_STYLE}]], {
2619
2647
  click: (evt) => {
@@ -2633,6 +2661,20 @@ function refresh_legend(gobj)
2633
2661
  }
2634
2662
  }
2635
2663
 
2664
+ /************************************************************
2665
+ * The application changed language. `refresh_language()` has
2666
+ * already been over the document by now (the shell runs it
2667
+ * before publishing), so what is left is what carries no key:
2668
+ * the legend, whose chips say an ACTION that depends on the
2669
+ * state, and the find box's counted line.
2670
+ ************************************************************/
2671
+ function ac_language_changed(gobj, event, kw, src)
2672
+ {
2673
+ refresh_legend(gobj);
2674
+ refresh_find_result(gobj);
2675
+ return 0;
2676
+ }
2677
+
2636
2678
  /************************************************************
2637
2679
  * The engine said what is drawn: repaint the strip.
2638
2680
  ************************************************************/
@@ -2848,6 +2890,18 @@ function ac_toggle_node_labels(gobj, event, kw, src)
2848
2890
  ************************************************************/
2849
2891
  function ac_find_result(gobj, event, kw, src)
2850
2892
  {
2893
+ /* Kept, because the line is COMPOSED -- a counted word and a
2894
+ * count of hidden topics -- so no i18n key in the DOM can
2895
+ * re-translate it: its owner repaints it from the last answer
2896
+ * when the language changes. */
2897
+ gobj.priv._find_result = kw || null;
2898
+ refresh_find_result(gobj);
2899
+ return 0;
2900
+ }
2901
+
2902
+ function refresh_find_result(gobj)
2903
+ {
2904
+ let kw = gobj.priv._find_result;
2851
2905
  let $container = gobj_read_attr(gobj, "$container");
2852
2906
  if(!$container) {
2853
2907
  return 0;
@@ -3004,6 +3058,7 @@ function create_gclass(gclass_name)
3004
3058
  ["EV_SHOW", ac_show, null],
3005
3059
  ["EV_HIDE", ac_hide, null],
3006
3060
  ["EV_TRANSPORT_STATE", ac_transport_state, null],
3061
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
3007
3062
  ]]
3008
3063
  ];
3009
3064
 
@@ -3052,6 +3107,7 @@ function create_gclass(gclass_name)
3052
3107
  event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
3053
3108
  ["EV_SHOW", 0],
3054
3109
  ["EV_HIDE", 0],
3110
+ ["EV_LANGUAGE_CHANGED", 0],
3055
3111
  ["EV_TRANSPORT_STATE", 0],
3056
3112
  ];
3057
3113
 
@@ -51,8 +51,10 @@
51
51
  *
52
52
  * `radial`: the root in the middle and a ring per depth, each
53
53
  * subtree owning a sector proportional to its leaves; the radius
54
- * of a ring grows until its neighbours sit side by side, so
55
- * nothing overlaps by construction (see `layout_radial`).
54
+ * of a ring grows until its neighbours sit side by side AND
55
+ * until it clears the ring inside it, so nothing overlaps by
56
+ * construction, across the ring or along the radius (see
57
+ * `layout_radial`).
56
58
  *
57
59
  * Pure: takes plain nodes and edges, returns positions. The G6
58
60
  * classes in c_g6_nodes_tree.js are two thin adapters over it.
@@ -272,6 +274,18 @@ function tidy_tree_lr(nodes, edges, o)
272
274
  return pos;
273
275
  }
274
276
 
277
+ /* How much of a card lies along the direction `a`: the projection
278
+ * of its rectangle on that direction. Full width when the card is
279
+ * read along its width, full height across it, and the honest
280
+ * mixture in between. */
281
+ function extent_at(n, a)
282
+ {
283
+ if(!n) {
284
+ return 0;
285
+ }
286
+ return Math.abs(n.w * Math.cos(a)) + Math.abs(n.h * Math.sin(a));
287
+ }
288
+
275
289
  /************************************************************
276
290
  * The radial tree: the root in the middle, a ring per depth.
277
291
  *
@@ -333,10 +347,10 @@ export function layout_radial(nodes, edges, opts)
333
347
  /* An explicit stack, not recursion: the sectors of a ring are
334
348
  * sorted by angle in pass 2, so the order they are pushed in does
335
349
  * not matter -- only that every node gets its own. */
336
- let sector = (root_id, root_from, root_span) => {
337
- let stack = [{id: root_id, from: root_from, span: root_span}];
350
+ let sector = (root_id, root_from, root_span, root_parent) => {
351
+ let stack = [{id: root_id, from: root_from, span: root_span, parent: root_parent}];
338
352
  while(stack.length) {
339
- let {id, from, span} = stack.pop();
353
+ let {id, from, span, parent} = stack.pop();
340
354
  let a = from + span / 2;
341
355
  angle.set(id, a);
342
356
  let ring = ring_of(id);
@@ -344,11 +358,21 @@ export function layout_radial(nodes, edges, opts)
344
358
  rings[ring] = [];
345
359
  }
346
360
  let n = t.by_id.get(id);
347
- rings[ring].push({id: id, angle: a, diag: Math.hypot(n.w, n.h)});
361
+ rings[ring].push({
362
+ id: id,
363
+ angle: a,
364
+ diag: Math.hypot(n.w, n.h),
365
+ /* How much of the card lies along the RADIUS at this
366
+ * angle -- the projection of the rectangle on that
367
+ * direction -- for itself and for the node it hangs
368
+ * from, which is the one it has to clear. */
369
+ ext: extent_at(n, a),
370
+ parent_ext: parent? extent_at(t.by_id.get(parent), a) : 0,
371
+ });
348
372
  let cursor = from;
349
373
  for(let kid of (t.children.get(id) || [])) {
350
374
  let part = span * leaves.get(kid) / leaves.get(id);
351
- stack.push({id: kid, from: cursor, span: part});
375
+ stack.push({id: kid, from: cursor, span: part, parent: id});
352
376
  cursor += part;
353
377
  }
354
378
  }
@@ -359,28 +383,43 @@ export function layout_radial(nodes, edges, opts)
359
383
  let cursor = -Math.PI / 2; /* the first child at the top */
360
384
  for(let kid of (t.children.get(root) || [])) {
361
385
  let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
362
- sector(kid, cursor, part);
386
+ sector(kid, cursor, part, root);
363
387
  cursor += part;
364
388
  }
365
389
  } else {
366
390
  let cursor = -Math.PI / 2;
367
391
  for(let root of t.roots) {
368
392
  let part = 2 * Math.PI * leaves.get(root) / total;
369
- sector(root, cursor, part);
393
+ sector(root, cursor, part, null);
370
394
  cursor += part;
371
395
  }
372
396
  }
373
397
 
374
- /* Pass 2, the RADII: a ring is one step further out than the
375
- * ring before, and as far out as its two closest neighbours need
376
- * to sit side by side -- the arc between two adjacent centres is
377
- * r·Δangle, and it has to hold half of each card plus the gap.
378
- * Measured on NEIGHBOURS, not on the ring's total length: a
379
- * leafless root squeezed between two big subtrees gets a sliver
380
- * of angle, and it is the sliver that sets the radius. */
398
+ /* Pass 2, the RADII. Two constraints, and the larger wins.
399
+ *
400
+ * ACROSS the ring: its two closest neighbours have to sit side
401
+ * by side -- the arc between two adjacent centres is r·Δangle,
402
+ * and it has to hold half of each card plus the gap. Measured on
403
+ * NEIGHBOURS, not on the ring's total length: a leafless root
404
+ * squeezed between two big subtrees gets a sliver of angle, and
405
+ * it is the sliver that sets the radius.
406
+ *
407
+ * ALONG the radius: `ranksep` is a step between CENTRES, so a
408
+ * ring one step out was one step out whatever the cards
409
+ * measured -- 180 between the centres of two cards that reach
410
+ * 95 each way is two cards touching. The tidy tree never had
411
+ * this: its `ranksep` is a gap between COLUMNS, and a column
412
+ * carries its own width. Here every card is measured against
413
+ * the one it HANGS FROM, in the direction it hangs in -- the
414
+ * two are radially aligned, or nearly, since a child lives
415
+ * inside its parent's sector -- and the ring goes out far
416
+ * enough for the worst of them. */
381
417
  let radius = [0];
382
418
  for(let ring = 1; ring < rings.length; ring++) {
383
419
  let r = radius[ring - 1] + o.ranksep;
420
+ for(let n of (rings[ring] || [])) {
421
+ r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
422
+ }
384
423
  let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
385
424
  for(let i = 0; i < list.length && list.length > 1; i++) {
386
425
  let a = list[i];
@@ -181,6 +181,21 @@ describe("the radial tree", () => {
181
181
  expect(d).toBeGreaterThan(190);
182
182
  });
183
183
 
184
+ test("a ring clears the ring inside it: a card does not sit on its parent", () => {
185
+ /* One child, straight below its root: `ranksep` alone is a
186
+ * step between CENTRES, so two cards 96 tall 100 apart had
187
+ * 48 + 48 of card and 4 of air. The ring adds the halves. */
188
+ let pos = layout_radial(
189
+ [N("root", 172, 96), N("kid", 172, 96)],
190
+ [{source: "root", target: "kid", rank: 0}],
191
+ {nodesep: 18, ranksep: 100}
192
+ );
193
+ let d = Math.hypot(
194
+ pos.get("kid").x - pos.get("root").x, pos.get("kid").y - pos.get("root").y
195
+ );
196
+ expect(d).toBeGreaterThanOrEqual(48 + 48 + 18);
197
+ });
198
+
184
199
  test("several roots share the circle around an empty centre", () => {
185
200
  let pos = layout_radial(
186
201
  [N("a", 100, 50), N("b", 100, 50), N("c", 100, 50)], [], {ranksep: 100}