@yuneta/gobj-ui 7.23.17 → 7.23.19
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/dist/gobj-ui.cjs.js +567 -170
- package/dist/gobj-ui.es.js +559 -162
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +135 -3
- package/src/c_yui_gobj_tree_js.js +185 -7
- package/src/c_yui_json.css +11 -2
- package/src/c_yui_json_graph.js +237 -30
- package/src/lib_icons.js +7 -0
- package/src/yui_graph_camera.js +95 -0
package/src/c_yui_json_graph.js
CHANGED
|
@@ -50,6 +50,9 @@ import {yui_toolbar} from "./yui_toolbar.js";
|
|
|
50
50
|
import {attach_clear} from "./yui_inputs.js";
|
|
51
51
|
import {
|
|
52
52
|
yui_graph_camera_items,
|
|
53
|
+
yui_graph_anchor_item,
|
|
54
|
+
yui_graph_update_anchor,
|
|
55
|
+
yui_graph_center_on,
|
|
53
56
|
yui_graph_fold_items,
|
|
54
57
|
yui_graph_refresh_item,
|
|
55
58
|
yui_graph_update_zoom,
|
|
@@ -190,6 +193,12 @@ let PRIVATE_DATA = {
|
|
|
190
193
|
$find_input: null,
|
|
191
194
|
$find_result: null,
|
|
192
195
|
$find_count: null,
|
|
196
|
+
|
|
197
|
+
/*---------------- camera anchor ----------------*/
|
|
198
|
+
anchor_id: "", // the node the camera keeps in the middle
|
|
199
|
+
anchor_path: "", // its path, so a rebuild can find it again
|
|
200
|
+
anchor_state: "off", // "off" | "arming" | "on"
|
|
201
|
+
last_zoom: 1, // tells a wheel zoom from a drag in aftertransform
|
|
193
202
|
};
|
|
194
203
|
|
|
195
204
|
let __gclass__ = null;
|
|
@@ -197,17 +206,41 @@ let __layout_registered__ = false;
|
|
|
197
206
|
|
|
198
207
|
|
|
199
208
|
/***************************************************************
|
|
200
|
-
* Type
|
|
209
|
+
* Type colours, ONE EXPLICIT PALETTE PER THEME and not a
|
|
210
|
+
* derivation, for the same reason the tree keeps two sets of
|
|
211
|
+
* CSS tokens: a colour that reads on white is not the same
|
|
212
|
+
* colour lightened, and deriving one from the other made the
|
|
213
|
+
* two views of one document disagree about what a string is.
|
|
214
|
+
* These ARE the tree's tokens (`c_yui_json.css`), so the tree
|
|
215
|
+
* and the graph now say green with the same green.
|
|
216
|
+
*
|
|
217
|
+
* The light values are darker than the `mx_json_viewer` ones
|
|
218
|
+
* they came from, because those could not be read: `#FF8C00`
|
|
219
|
+
* is mid-luminance, so it reaches at most **2.33:1 against any
|
|
220
|
+
* background that exists** -- no card colour and no highlight
|
|
221
|
+
* could have fixed it, only the colour itself. Orange and blue
|
|
222
|
+
* moved until every type clears 4.5:1 on all five surfaces this
|
|
223
|
+
* viewer paints (two light cards, the match chip, two dark
|
|
224
|
+
* cards); red and green were already there.
|
|
201
225
|
***************************************************************/
|
|
202
|
-
const
|
|
226
|
+
const TYPE_COLORS_LIGHT = {
|
|
203
227
|
string: "#006000", // green
|
|
204
|
-
number: "#
|
|
205
|
-
boolean: "#
|
|
206
|
-
null: "#
|
|
228
|
+
number: "#b5330b", // red
|
|
229
|
+
boolean: "#8C4D00", // orange
|
|
230
|
+
null: "#4359C6", // blue
|
|
207
231
|
list: "#4C0099", // purple
|
|
208
232
|
dict: "#4C0099", // purple
|
|
209
233
|
};
|
|
210
234
|
|
|
235
|
+
const TYPE_COLORS_DARK = {
|
|
236
|
+
string: "#7ec87e",
|
|
237
|
+
number: "#ff8a65",
|
|
238
|
+
boolean: "#ffb74d",
|
|
239
|
+
null: "#90a4f4",
|
|
240
|
+
list: "#B799D6",
|
|
241
|
+
dict: "#B799D6",
|
|
242
|
+
};
|
|
243
|
+
|
|
211
244
|
/***************************************************************
|
|
212
245
|
* Group (card) colours. `fill` is the light-theme card; `tint`
|
|
213
246
|
* is the hue that fill hints at, used to derive the dark card —
|
|
@@ -260,24 +293,17 @@ function json_card_style(group, dark)
|
|
|
260
293
|
}
|
|
261
294
|
|
|
262
295
|
/***************************************************************
|
|
263
|
-
* A scalar's colour by type
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* card (the worst type goes from 2.88:1 to 4.74:1) and washes the
|
|
270
|
-
* six types toward one pastel, and past this point red, orange
|
|
271
|
-
* and purple stop being tellable apart, which is the only reason
|
|
272
|
-
* they are coloured at all.
|
|
296
|
+
* A scalar's colour by type, straight out of the palette for
|
|
297
|
+
* the theme. It used to be the light colour mixed with white,
|
|
298
|
+
* which is how a document ended up green in one view and a
|
|
299
|
+
* paler green in the other, and how the amount of white became
|
|
300
|
+
* a knob nobody could set: enough of it to read on a dark card
|
|
301
|
+
* washed the six types into one pastel.
|
|
273
302
|
***************************************************************/
|
|
274
303
|
function type_color(type, dark)
|
|
275
304
|
{
|
|
276
|
-
let
|
|
277
|
-
|
|
278
|
-
return color;
|
|
279
|
-
}
|
|
280
|
-
return `color-mix(in srgb, ${color} 40%, #ffffff)`;
|
|
305
|
+
let palette = dark? TYPE_COLORS_DARK: TYPE_COLORS_LIGHT;
|
|
306
|
+
return palette[type] || (dark ? "#e8eaed" : "black");
|
|
281
307
|
}
|
|
282
308
|
|
|
283
309
|
|
|
@@ -575,6 +601,7 @@ function make_toolbar(gobj)
|
|
|
575
601
|
* the same console and a copied toolbar drifts.
|
|
576
602
|
*/
|
|
577
603
|
center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
|
|
604
|
+
center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
|
|
578
605
|
center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
|
|
579
606
|
|
|
580
607
|
/*
|
|
@@ -680,6 +707,30 @@ function build_graph(gobj)
|
|
|
680
707
|
* after every notch. */
|
|
681
708
|
graph.on('aftertransform', () => {
|
|
682
709
|
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
710
|
+
|
|
711
|
+
/*
|
|
712
|
+
* A ZOOM re-centres on the anchor; a PAN does not.
|
|
713
|
+
* `aftertransform` fires for both, and an anchor that also
|
|
714
|
+
* answered a drag would make the graph impossible to move
|
|
715
|
+
* while it is set. The zoom LEVEL is what tells them apart:
|
|
716
|
+
* it changes on a wheel notch and never on a drag.
|
|
717
|
+
*
|
|
718
|
+
* No recursion: the centring translates without zooming, so
|
|
719
|
+
* the level it fires back with is the one just stored.
|
|
720
|
+
*/
|
|
721
|
+
let z = priv.last_zoom;
|
|
722
|
+
try {
|
|
723
|
+
z = priv.graph.getZoom();
|
|
724
|
+
} catch(e) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if(z === priv.last_zoom) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
priv.last_zoom = z;
|
|
731
|
+
if(priv.anchor_state === "on" && priv.anchor_id) {
|
|
732
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
733
|
+
}
|
|
683
734
|
});
|
|
684
735
|
|
|
685
736
|
graph.on(CanvasEvent.CLICK, (evt) => {
|
|
@@ -943,16 +994,33 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
|
|
|
943
994
|
card_matched = true;
|
|
944
995
|
priv.match_count++;
|
|
945
996
|
}
|
|
946
|
-
let html = build_cell_html(key, value, type, dark, matched);
|
|
947
|
-
lines.push(html);
|
|
948
997
|
|
|
998
|
+
/*
|
|
999
|
+
* A non-empty container gets a CARD OF ITS OWN, so a row for it
|
|
1000
|
+
* here would be the same thing drawn twice -- and the second
|
|
1001
|
+
* drawing is the one that does not scale. An array of 14 dicts
|
|
1002
|
+
* printed fourteen `0: {13}` rows above fourteen cards saying
|
|
1003
|
+
* the same; an array of a thousand prints a thousand-row card
|
|
1004
|
+
* nobody can read, beside the thousand cards it duplicates.
|
|
1005
|
+
*
|
|
1006
|
+
* What is left in the body is exactly the SCALARS: what this
|
|
1007
|
+
* node IS. What it CONTAINS is the edges, and the count now
|
|
1008
|
+
* rides in the header for the case where the body comes out
|
|
1009
|
+
* empty.
|
|
1010
|
+
*/
|
|
949
1011
|
if((is_object(value) && json_object_size(value) > 0) ||
|
|
950
1012
|
(is_array(value) && value.length > 0)) {
|
|
951
1013
|
pending_complex.push({key: key, value: value});
|
|
1014
|
+
continue;
|
|
952
1015
|
}
|
|
1016
|
+
|
|
1017
|
+
lines.push(build_cell_html(key, value, type, dark, matched));
|
|
953
1018
|
}
|
|
954
1019
|
|
|
955
1020
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
1021
|
+
if(pending_complex.length > 0) {
|
|
1022
|
+
label += is_dict? ` {${pending_complex.length}}`: ` [${pending_complex.length}]`;
|
|
1023
|
+
}
|
|
956
1024
|
|
|
957
1025
|
/*
|
|
958
1026
|
* Folded: keep the card, drop the branch, and SAY how much went
|
|
@@ -1086,12 +1154,36 @@ function load_json(gobj)
|
|
|
1086
1154
|
priv.pending_preserve_view = false;
|
|
1087
1155
|
|
|
1088
1156
|
if(nodes.length > 0) {
|
|
1157
|
+
let root_id = nodes[0].id; /* built before its children */
|
|
1089
1158
|
graph.setData({nodes: nodes, edges: edges});
|
|
1090
1159
|
graph.render().then(() => {
|
|
1091
|
-
|
|
1092
|
-
|
|
1160
|
+
/* Ids are generated per build: point the anchor at its path
|
|
1161
|
+
* again before anybody asks the camera to go there. */
|
|
1162
|
+
reanchor(gobj);
|
|
1163
|
+
|
|
1164
|
+
if(preserve_view) {
|
|
1165
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
1166
|
+
return;
|
|
1093
1167
|
}
|
|
1094
|
-
|
|
1168
|
+
|
|
1169
|
+
/*
|
|
1170
|
+
* ACTUAL SIZE, not fit. A JSON graph of anything real does
|
|
1171
|
+
* not fit at a zoom anybody can read: the schema of one
|
|
1172
|
+
* topic fits at 37%, where every card is grey texture.
|
|
1173
|
+
* Fitting answered "where is everything" when the question
|
|
1174
|
+
* on opening is "what does this say".
|
|
1175
|
+
*
|
|
1176
|
+
* Centred on the ANCHOR if there is one, else on the ROOT
|
|
1177
|
+
* -- never parked at the layout's origin, which is a corner
|
|
1178
|
+
* with nothing in it and is where `1:1` used to land.
|
|
1179
|
+
*/
|
|
1180
|
+
Promise.resolve(graph.zoomTo(1)).then(() => {
|
|
1181
|
+
if(!yui_graph_center_on(graph, priv.anchor_id)) {
|
|
1182
|
+
yui_graph_center_on(graph, root_id);
|
|
1183
|
+
}
|
|
1184
|
+
priv.last_zoom = 1;
|
|
1185
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
1186
|
+
});
|
|
1095
1187
|
});
|
|
1096
1188
|
}
|
|
1097
1189
|
}
|
|
@@ -1473,6 +1565,93 @@ function ac_theme(gobj, event, kw, src)
|
|
|
1473
1565
|
return 0;
|
|
1474
1566
|
}
|
|
1475
1567
|
|
|
1568
|
+
/************************************************************
|
|
1569
|
+
* Every camera action ends the same way once an anchor is
|
|
1570
|
+
* set: the thing you were reading goes back to the middle.
|
|
1571
|
+
*
|
|
1572
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
1573
|
+
* -- centring before the zoom has finished centres the OLD
|
|
1574
|
+
* view, and the reader watches the node slide away and come
|
|
1575
|
+
* back.
|
|
1576
|
+
************************************************************/
|
|
1577
|
+
function after_camera(gobj, moved)
|
|
1578
|
+
{
|
|
1579
|
+
let priv = gobj.priv;
|
|
1580
|
+
|
|
1581
|
+
if(priv.anchor_state !== "on" || !priv.anchor_id) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
Promise.resolve(moved).then(function() {
|
|
1585
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
1586
|
+
}).catch(function() {
|
|
1587
|
+
/* a camera move that never finished has nothing to centre */
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/************************************************************
|
|
1592
|
+
* Point the anchor back at its node after a rebuild.
|
|
1593
|
+
*
|
|
1594
|
+
* Node ids are generated per build, so the id the reader
|
|
1595
|
+
* chose does not survive a refresh, a fold or a layout
|
|
1596
|
+
* change -- the PATH does, and it is what the anchor really
|
|
1597
|
+
* means: "this place in the document", not "this card".
|
|
1598
|
+
*
|
|
1599
|
+
* A path that is no longer drawn (its parent got folded)
|
|
1600
|
+
* releases the anchor rather than leaving a button that says
|
|
1601
|
+
* it is locked onto something invisible.
|
|
1602
|
+
************************************************************/
|
|
1603
|
+
function reanchor(gobj)
|
|
1604
|
+
{
|
|
1605
|
+
let priv = gobj.priv;
|
|
1606
|
+
|
|
1607
|
+
if(priv.anchor_state !== "on" || !priv.anchor_path) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
let found = "";
|
|
1612
|
+
try {
|
|
1613
|
+
for(let node of priv.graph.getNodeData()) {
|
|
1614
|
+
if(node && node.data && node.data.path === priv.anchor_path) {
|
|
1615
|
+
found = node.id;
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
} catch(e) {
|
|
1620
|
+
return; /* between renders: keep what we have and try later */
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
priv.anchor_id = found;
|
|
1624
|
+
if(!found) {
|
|
1625
|
+
priv.anchor_state = "off";
|
|
1626
|
+
priv.anchor_path = "";
|
|
1627
|
+
}
|
|
1628
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/************************************************************
|
|
1632
|
+
* Arm the anchor, or let it go.
|
|
1633
|
+
*
|
|
1634
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
1635
|
+
* target it starts waiting for one, and with a target it
|
|
1636
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
1637
|
+
* what somebody who pressed it by mistake will try.
|
|
1638
|
+
************************************************************/
|
|
1639
|
+
function ac_toggle_anchor(gobj, event, kw, src)
|
|
1640
|
+
{
|
|
1641
|
+
let priv = gobj.priv;
|
|
1642
|
+
|
|
1643
|
+
if(priv.anchor_state === "off") {
|
|
1644
|
+
priv.anchor_state = "arming";
|
|
1645
|
+
} else {
|
|
1646
|
+
priv.anchor_state = "off";
|
|
1647
|
+
priv.anchor_id = "";
|
|
1648
|
+
priv.anchor_path = "";
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
1652
|
+
return 0;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1476
1655
|
/************************************************************
|
|
1477
1656
|
*
|
|
1478
1657
|
************************************************************/
|
|
@@ -1481,7 +1660,7 @@ function ac_zoom_in(gobj, event, kw, src)
|
|
|
1481
1660
|
let graph = gobj.priv.graph;
|
|
1482
1661
|
if(graph) {
|
|
1483
1662
|
let z = graph.getZoom();
|
|
1484
|
-
graph.zoomTo(z * 1.2);
|
|
1663
|
+
after_camera(gobj, graph.zoomTo(z * 1.2));
|
|
1485
1664
|
}
|
|
1486
1665
|
return 0;
|
|
1487
1666
|
}
|
|
@@ -1494,7 +1673,7 @@ function ac_zoom_out(gobj, event, kw, src)
|
|
|
1494
1673
|
let graph = gobj.priv.graph;
|
|
1495
1674
|
if(graph) {
|
|
1496
1675
|
let z = graph.getZoom();
|
|
1497
|
-
graph.zoomTo(z * 0.8);
|
|
1676
|
+
after_camera(gobj, graph.zoomTo(z * 0.8));
|
|
1498
1677
|
}
|
|
1499
1678
|
return 0;
|
|
1500
1679
|
}
|
|
@@ -1504,10 +1683,21 @@ function ac_zoom_out(gobj, event, kw, src)
|
|
|
1504
1683
|
************************************************************/
|
|
1505
1684
|
function ac_zoom_reset(gobj, event, kw, src)
|
|
1506
1685
|
{
|
|
1507
|
-
let
|
|
1686
|
+
let priv = gobj.priv;
|
|
1687
|
+
let graph = priv.graph;
|
|
1688
|
+
|
|
1508
1689
|
if(graph && graph.rendered) {
|
|
1509
|
-
|
|
1510
|
-
|
|
1690
|
+
/*
|
|
1691
|
+
* With an anchor, actual size means "this node, life size".
|
|
1692
|
+
* Without one it means the layout's origin, which is where
|
|
1693
|
+
* `1:1` always landed -- a corner nobody was looking at.
|
|
1694
|
+
*/
|
|
1695
|
+
if(priv.anchor_state === "on" && priv.anchor_id) {
|
|
1696
|
+
after_camera(gobj, graph.zoomTo(1));
|
|
1697
|
+
} else {
|
|
1698
|
+
graph.zoomTo(1);
|
|
1699
|
+
graph.translateTo([0, 0]);
|
|
1700
|
+
}
|
|
1511
1701
|
}
|
|
1512
1702
|
return 0;
|
|
1513
1703
|
}
|
|
@@ -1539,6 +1729,21 @@ function ac_node_click(gobj, event, kw, src)
|
|
|
1539
1729
|
nodedata = graph.getNodeData(node_id);
|
|
1540
1730
|
} catch(e) {}
|
|
1541
1731
|
|
|
1732
|
+
/*
|
|
1733
|
+
* A waiting anchor TAKES the click: it is what the reader armed
|
|
1734
|
+
* the button for, and letting the same press also open the item
|
|
1735
|
+
* would make picking a target a side effect of doing something
|
|
1736
|
+
* else.
|
|
1737
|
+
*/
|
|
1738
|
+
if(priv.anchor_state === "arming") {
|
|
1739
|
+
priv.anchor_id = node_id;
|
|
1740
|
+
priv.anchor_path = (nodedata && nodedata.data)? nodedata.data.path: "";
|
|
1741
|
+
priv.anchor_state = "on";
|
|
1742
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
1743
|
+
yui_graph_center_on(graph, node_id);
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1542
1747
|
if(nodedata && nodedata.data) {
|
|
1543
1748
|
gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", {
|
|
1544
1749
|
path: nodedata.data.path,
|
|
@@ -1660,6 +1865,7 @@ function create_gclass(gclass_name)
|
|
|
1660
1865
|
["EV_ZOOM_RESET", ac_zoom_reset, null],
|
|
1661
1866
|
["EV_CENTER", ac_center, null],
|
|
1662
1867
|
["EV_NODE_CLICK", ac_node_click, null],
|
|
1868
|
+
["EV_TOGGLE_ANCHOR", ac_toggle_anchor, null],
|
|
1663
1869
|
["EV_RESIZE", ac_resize, null],
|
|
1664
1870
|
["EV_SHOW", ac_show, null],
|
|
1665
1871
|
["EV_HIDE", ac_hide, null],
|
|
@@ -1683,6 +1889,7 @@ function create_gclass(gclass_name)
|
|
|
1683
1889
|
["EV_ZOOM_RESET", 0],
|
|
1684
1890
|
["EV_CENTER", 0],
|
|
1685
1891
|
["EV_NODE_CLICK", 0],
|
|
1892
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
1686
1893
|
["EV_RESIZE", 0],
|
|
1687
1894
|
["EV_SHOW", 0],
|
|
1688
1895
|
["EV_HIDE", 0],
|
package/src/lib_icons.js
CHANGED
|
@@ -140,6 +140,13 @@ const ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
140
140
|
stroke="currentColor" stroke-width="40"
|
|
141
141
|
stroke-dasharray="59 36"/>
|
|
142
142
|
</symbol>
|
|
143
|
+
<!-- location-crosshairs (anchor the camera on one element).
|
|
144
|
+
The SAME path as the DOM toolbars and their yi-location-crosshairs
|
|
145
|
+
mask: the two toolbars sit in one console, and a control that
|
|
146
|
+
does one thing has to be drawn one way. -->
|
|
147
|
+
<symbol id="g6-icon-anchor" viewBox="0 0 576 512">
|
|
148
|
+
<path fill="currentColor" d="M288-16c17.7 0 32 14.3 32 32l0 18.3c98.1 14 175.7 91.6 189.7 189.7l18.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-18.3 0c-14 98.1-91.6 175.7-189.7 189.7l0 18.3c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.3C157.9 463.7 80.3 386.1 66.3 288L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l18.3 0C80.3 125.9 157.9 48.3 256 34.3L256 16c0-17.7 14.3-32 32-32zM128 256a160 160 0 1 0 320 0 160 160 0 1 0 -320 0zm160-96a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/>
|
|
149
|
+
</symbol>
|
|
143
150
|
<!-- floppy-disk (save) -->
|
|
144
151
|
<symbol id="g6-icon-save" viewBox="0 0 448 512">
|
|
145
152
|
<path fill="currentColor" d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-242.7c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32L64 32zm32 96c0-17.7 14.3-32 32-32l160 0c17.7 0 32 14.3 32 32l0 64c0 17.7-14.3 32-32 32l-160 0c-17.7 0-32-14.3-32-32l0-64zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/>
|
package/src/yui_graph_camera.js
CHANGED
|
@@ -39,6 +39,8 @@ import {createElement2, gobj_send_event} from "@yuneta/gobj-js";
|
|
|
39
39
|
|
|
40
40
|
import {t} from "i18next";
|
|
41
41
|
|
|
42
|
+
import {set_pressed_state} from "./lib_graph.js";
|
|
43
|
+
|
|
42
44
|
|
|
43
45
|
/************************************************************
|
|
44
46
|
* One icon button that fires `event_name`.
|
|
@@ -141,6 +143,99 @@ export function yui_graph_fold_items(gobj, wide)
|
|
|
141
143
|
];
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/************************************************************
|
|
147
|
+
* The ANCHOR toggle: pick one element and every camera move
|
|
148
|
+
* from then on leaves it in the MIDDLE.
|
|
149
|
+
*
|
|
150
|
+
* Why it exists: a graph that fits on screen is unreadable at
|
|
151
|
+
* the zoom that makes it fit, and the zoom that makes it
|
|
152
|
+
* readable does not fit -- so the useful view is always a
|
|
153
|
+
* fraction of the document, and which fraction is the whole
|
|
154
|
+
* question. Without an anchor, `1:1` lands wherever the
|
|
155
|
+
* layout's origin happens to be and the reader hunts for the
|
|
156
|
+
* node they were looking at.
|
|
157
|
+
*
|
|
158
|
+
* Crosshairs and not a thumbtack. A tack says "do not move
|
|
159
|
+
* this", which is what a pinned NODE would mean in a graph you
|
|
160
|
+
* can drag nodes around in; this pins the CAMERA to a target,
|
|
161
|
+
* which is what a crosshair says and what every map that
|
|
162
|
+
* offers it draws. It is also in the icon set already, and a
|
|
163
|
+
* `yi-*` class that is not is a black square.
|
|
164
|
+
*
|
|
165
|
+
* Three states, because two would not say what a press does:
|
|
166
|
+
*
|
|
167
|
+
* off no target; the camera behaves as it always did
|
|
168
|
+
* arming waiting for you to click the element you mean
|
|
169
|
+
* on locked; every zoom re-centres on it
|
|
170
|
+
************************************************************/
|
|
171
|
+
export function yui_graph_anchor_item(gobj, wide)
|
|
172
|
+
{
|
|
173
|
+
return camera_button(gobj, "yi-location-crosshairs", "EV_TOGGLE_ANCHOR",
|
|
174
|
+
"anchor view", wide);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/************************************************************
|
|
178
|
+
* Paint the anchor button. `state` is "off" | "arming" | "on".
|
|
179
|
+
*
|
|
180
|
+
* Both live states look PRESSED, because both are states the
|
|
181
|
+
* control is IN rather than actions; what separates them is the
|
|
182
|
+
* colour, and only the waiting one takes a palette colour --
|
|
183
|
+
* it is the one that needs the reader to do something next.
|
|
184
|
+
************************************************************/
|
|
185
|
+
export function yui_graph_update_anchor($container, state)
|
|
186
|
+
{
|
|
187
|
+
if(!$container) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let $btn = $container.querySelector('.EV_TOGGLE_ANCHOR');
|
|
191
|
+
if(!$btn) {
|
|
192
|
+
return; /* a toolbar without the control: nothing to paint */
|
|
193
|
+
}
|
|
194
|
+
set_pressed_state($container, '.EV_TOGGLE_ANCHOR', state !== "off");
|
|
195
|
+
$btn.classList.toggle('color_pending_state', state === "arming");
|
|
196
|
+
$btn.setAttribute("aria-pressed", (state === "on")? "true": "false");
|
|
197
|
+
|
|
198
|
+
let key = (state === "arming")? "click the element to centre on"
|
|
199
|
+
: (state === "on")? "centred: click to release"
|
|
200
|
+
: "anchor view";
|
|
201
|
+
$btn.setAttribute("title", t(key));
|
|
202
|
+
$btn.setAttribute("data-i18n-title", key);
|
|
203
|
+
$btn.setAttribute("aria-label", t(key));
|
|
204
|
+
$btn.setAttribute("data-i18n-aria-label", key);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/************************************************************
|
|
208
|
+
* Put `node_id` back in the middle of the viewport.
|
|
209
|
+
*
|
|
210
|
+
* Called AFTER the camera moved, never instead of moving it:
|
|
211
|
+
* the zoom is what the reader asked for, the centring is what
|
|
212
|
+
* keeps the thing they are reading under their eyes.
|
|
213
|
+
*
|
|
214
|
+
* Guarded on every side because it runs on every wheel notch:
|
|
215
|
+
* a graph mid-rebuild, an anchor whose node went away with a
|
|
216
|
+
* fold, a G6 version without `focusElement` -- none of those is
|
|
217
|
+
* an error worth a log line per notch, they are all "there is
|
|
218
|
+
* nothing to centre right now".
|
|
219
|
+
************************************************************/
|
|
220
|
+
export function yui_graph_center_on(graph, node_id)
|
|
221
|
+
{
|
|
222
|
+
if(!graph || !node_id) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if(typeof graph.getElementData === "function" && !graph.getElementData(node_id)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if(typeof graph.focusElement !== "function") {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
graph.focusElement(node_id);
|
|
233
|
+
return true;
|
|
234
|
+
} catch(e) {
|
|
235
|
+
return false; /* the element is gone, or the graph is between renders */
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
144
239
|
/************************************************************
|
|
145
240
|
* A refresh button, for the graphs that offer one.
|
|
146
241
|
************************************************************/
|