@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.
- package/README.md +47 -7
- package/dist/gobj-ui.cjs.js +281 -104
- package/dist/gobj-ui.es.js +281 -104
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +267 -88
- package/src/c_yui_treedb_graph.js +59 -3
- package/src/treedb_layout.js +55 -16
- package/src/treedb_layout.test.js +15 -0
|
@@ -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
|
-
|
|
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'),
|
|
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
|
|
package/src/treedb_layout.js
CHANGED
|
@@ -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
|
|
55
|
-
* nothing overlaps by
|
|
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({
|
|
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
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
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}
|