@yuneta/gobj-ui 7.23.8 → 7.23.10
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 +75 -6
- package/dist/gobj-ui.cjs.js +625 -76
- package/dist/gobj-ui.es.js +626 -77
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +496 -68
- package/src/c_yui_gobj_tree_js.js +2 -0
- package/src/c_yui_json_graph.js +2 -0
- package/src/g6_brush_select_owned.js +69 -0
- package/src/g6_drag_canvas_touch.js +21 -0
- package/src/g6_touch_gestures.js +413 -0
- package/src/lib_graph.css +19 -0
- package/src/lib_icons.js +14 -0
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -103,6 +103,14 @@ import i18next, {t} from "i18next";
|
|
|
103
103
|
|
|
104
104
|
import {inject_svg_icons} from "./lib_icons.js";
|
|
105
105
|
import {ensure_drag_canvas_patch} from "./g6_drag_canvas_touch.js";
|
|
106
|
+
import {
|
|
107
|
+
ensure_brush_select_owned,
|
|
108
|
+
BRUSH_SELECT_OWNED,
|
|
109
|
+
} from "./g6_brush_select_owned.js";
|
|
110
|
+
import {
|
|
111
|
+
ensure_pinch_zoom_patch,
|
|
112
|
+
install_long_press_contextmenu,
|
|
113
|
+
} from "./g6_touch_gestures.js";
|
|
106
114
|
import {yui_theme_now, yui_watch_theme} from "./yui_theme.js";
|
|
107
115
|
|
|
108
116
|
/***************************************************************
|
|
@@ -172,6 +180,8 @@ class YuiToolbar extends Toolbar
|
|
|
172
180
|
}
|
|
173
181
|
register(ExtensionCategory.PLUGIN, 'yui-toolbar', YuiToolbar);
|
|
174
182
|
ensure_drag_canvas_patch();
|
|
183
|
+
ensure_brush_select_owned();
|
|
184
|
+
ensure_pinch_zoom_patch();
|
|
175
185
|
|
|
176
186
|
/***************************************************************
|
|
177
187
|
* Constants
|
|
@@ -336,6 +346,9 @@ let PRIVATE_DATA = {
|
|
|
336
346
|
_focus_topic: null, // topic currently focused (EV_FOCUS_TOPIC)
|
|
337
347
|
_focus_ids: [], // node ids carrying the focus 'active' state
|
|
338
348
|
_on_pointerdown_focus: null, // listener keeping the keyboard on the canvas
|
|
349
|
+
_uninstall_long_press: null, // touch door to the context menu
|
|
350
|
+
toolbar_collapsed: true, // floating toolbars folded (narrow only)
|
|
351
|
+
_toolbars_could_collapse: null, // last answer, to notice it changed
|
|
339
352
|
_on_focusout_restore: null, // ...and putting it back when it goes nowhere
|
|
340
353
|
_selected_paint_ids: [], // node ids whose card is PAINTED selected
|
|
341
354
|
// (G6's 'selected' state is the selection
|
|
@@ -491,6 +504,11 @@ function mt_destroy(gobj)
|
|
|
491
504
|
priv._resize_raf = 0;
|
|
492
505
|
}
|
|
493
506
|
|
|
507
|
+
if(priv._uninstall_long_press) {
|
|
508
|
+
priv._uninstall_long_press();
|
|
509
|
+
priv._uninstall_long_press = null;
|
|
510
|
+
}
|
|
511
|
+
|
|
494
512
|
if(priv.graph) {
|
|
495
513
|
priv.graph.destroy();
|
|
496
514
|
priv.graph = null;
|
|
@@ -943,6 +961,19 @@ function configure_plugins(gobj)
|
|
|
943
961
|
}
|
|
944
962
|
);
|
|
945
963
|
|
|
964
|
+
/*
|
|
965
|
+
* The menu above is the way to link, unlink, delete and resize,
|
|
966
|
+
* and `contextmenu` is a RIGHT CLICK -- G6 synthesises it from
|
|
967
|
+
* `pointerdown` with `button === 2` and reads nothing from the
|
|
968
|
+
* DOM event of the same name. So on a touch screen those
|
|
969
|
+
* commands had no door at all, whatever the browser does with a
|
|
970
|
+
* long press. Give them one.
|
|
971
|
+
*/
|
|
972
|
+
if(priv._uninstall_long_press) {
|
|
973
|
+
priv._uninstall_long_press();
|
|
974
|
+
}
|
|
975
|
+
priv._uninstall_long_press = install_long_press_contextmenu(priv.graph);
|
|
976
|
+
|
|
946
977
|
if(gobj_read_bool_attr(gobj, "with_toolbar")) {
|
|
947
978
|
configure_toolbar(gobj);
|
|
948
979
|
configure_toolbar_edit(gobj);
|
|
@@ -1006,6 +1037,67 @@ function update_toolbar(gobj)
|
|
|
1006
1037
|
* flipping now; the fallbacks are the old forced-light values, so
|
|
1007
1038
|
* a host without Bulma gets exactly what it had.
|
|
1008
1039
|
************************************************************/
|
|
1040
|
+
/************************************************************
|
|
1041
|
+
* Is the canvas too narrow to give both floating toolbars a
|
|
1042
|
+
* strip of it?
|
|
1043
|
+
*
|
|
1044
|
+
* They are drawn INSIDE the canvas, one on each edge. On a
|
|
1045
|
+
* desktop that is a corner; on a 356px-wide phone canvas the
|
|
1046
|
+
* two of them take a third of the drawing area and stand on
|
|
1047
|
+
* top of the nodes -- and a node under a toolbar cannot be
|
|
1048
|
+
* read, tapped or dragged out from under it.
|
|
1049
|
+
*
|
|
1050
|
+
* Measured on the CONTAINER, not on the window: the same
|
|
1051
|
+
* graph is a full page in one app and a card in a column in
|
|
1052
|
+
* another, and it is the box it actually got that decides.
|
|
1053
|
+
************************************************************/
|
|
1054
|
+
const TOOLBAR_COLLAPSE_WIDTH = 480;
|
|
1055
|
+
|
|
1056
|
+
function toolbars_can_collapse(gobj)
|
|
1057
|
+
{
|
|
1058
|
+
let $container = gobj.priv.$container;
|
|
1059
|
+
|
|
1060
|
+
if(!$container) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
let width = $container.getBoundingClientRect().width;
|
|
1064
|
+
if(!width) {
|
|
1065
|
+
return false; /* not laid out yet: decided on the first resize */
|
|
1066
|
+
}
|
|
1067
|
+
return width < TOOLBAR_COLLAPSE_WIDTH;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/************************************************************
|
|
1071
|
+
* Are they folded RIGHT NOW? Wide enough and the question
|
|
1072
|
+
* does not arise -- the desktop toolbar never grows a button
|
|
1073
|
+
* it does not need.
|
|
1074
|
+
************************************************************/
|
|
1075
|
+
function toolbars_collapsed(gobj)
|
|
1076
|
+
{
|
|
1077
|
+
if(!toolbars_can_collapse(gobj)) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
return gobj.priv.toolbar_collapsed !== false;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/************************************************************
|
|
1084
|
+
* The one item a folded toolbar shows, and the one an
|
|
1085
|
+
* unfolded narrow toolbar adds to fold itself again.
|
|
1086
|
+
************************************************************/
|
|
1087
|
+
function toolbar_fold_item(gobj)
|
|
1088
|
+
{
|
|
1089
|
+
if(toolbars_collapsed(gobj)) {
|
|
1090
|
+
return {
|
|
1091
|
+
id: 'g6-icon-toolbar-show', value: 'toolbar-toggle',
|
|
1092
|
+
className: 'EV_TOGGLE_TOOLBARS', title: t('show toolbar')
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
id: 'g6-icon-toolbar-hide', value: 'toolbar-toggle',
|
|
1097
|
+
className: 'EV_TOGGLE_TOOLBARS', title: t('hide toolbar')
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1009
1101
|
function toolbar_style(extra)
|
|
1010
1102
|
{
|
|
1011
1103
|
return Object.assign({
|
|
@@ -1092,6 +1184,12 @@ function configure_toolbar(gobj)
|
|
|
1092
1184
|
marginLeft: '12px',
|
|
1093
1185
|
}),
|
|
1094
1186
|
getItems: () => {
|
|
1187
|
+
/* Folded: ONE button, and it is the one that
|
|
1188
|
+
* unfolds. Everything else is behind it. */
|
|
1189
|
+
if(toolbars_collapsed(gobj)) {
|
|
1190
|
+
return [toolbar_fold_item(gobj)];
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1095
1193
|
let items = [
|
|
1096
1194
|
{ id: 'g6-icon-zoom-in', value: 'zoom-in', className: 'EV_ZOOM_IN', title: t('zoom in') },
|
|
1097
1195
|
{ id: 'g6-icon-zoom-out', value: 'zoom-out', className: 'EV_ZOOM_OUT', title: t('zoom out') },
|
|
@@ -1143,6 +1241,14 @@ function configure_toolbar(gobj)
|
|
|
1143
1241
|
}
|
|
1144
1242
|
}
|
|
1145
1243
|
|
|
1244
|
+
/* Getting the canvas back is a window control too,
|
|
1245
|
+
* and the last thing in the strip: it is where the
|
|
1246
|
+
* single folded button will be. */
|
|
1247
|
+
if(toolbars_can_collapse(gobj)) {
|
|
1248
|
+
items.push({ separator: true });
|
|
1249
|
+
items.push(toolbar_fold_item(gobj));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1146
1252
|
return items;
|
|
1147
1253
|
},
|
|
1148
1254
|
onClick: (value) => {
|
|
@@ -1171,6 +1277,9 @@ function configure_toolbar(gobj)
|
|
|
1171
1277
|
case 'exit-fullscreen':
|
|
1172
1278
|
gobj_send_event(gobj, "EV_EXIT_FULLSCREEN", {}, gobj);
|
|
1173
1279
|
break;
|
|
1280
|
+
case 'toolbar-toggle':
|
|
1281
|
+
gobj_send_event(gobj, "EV_TOGGLE_TOOLBARS", {}, gobj);
|
|
1282
|
+
break;
|
|
1174
1283
|
}
|
|
1175
1284
|
},
|
|
1176
1285
|
}
|
|
@@ -1185,7 +1294,11 @@ function configure_toolbar_edit(gobj)
|
|
|
1185
1294
|
{
|
|
1186
1295
|
let priv = gobj.priv;
|
|
1187
1296
|
|
|
1188
|
-
|
|
1297
|
+
/* Folded with the camera strip: the two of them are the same
|
|
1298
|
+
* wall over the canvas, and hiding one is half a fix. Removed
|
|
1299
|
+
* rather than emptied -- an empty toolbar is still a card with
|
|
1300
|
+
* a border sitting on the graph. */
|
|
1301
|
+
if(!priv.edit_mode || toolbars_collapsed(gobj)) {
|
|
1189
1302
|
// Remove edit toolbar when not in edit mode
|
|
1190
1303
|
graph_remove_plugin(gobj, 'toolbar-edit');
|
|
1191
1304
|
return;
|
|
@@ -1302,11 +1415,28 @@ function configure_behaviour(gobj)
|
|
|
1302
1415
|
/* Panning gives way while Shift is held: that is the
|
|
1303
1416
|
* marquee's gesture, and G6 binds drag-canvas straight
|
|
1304
1417
|
* to the drag events, so without this the canvas pans
|
|
1305
|
-
* under the rubber band.
|
|
1418
|
+
* under the rubber band.
|
|
1419
|
+
*
|
|
1420
|
+
* And it gives way to a NODE drag, which is what G6's
|
|
1421
|
+
* own default `enable` says and what this one has to
|
|
1422
|
+
* repeat: `enable` REPLACES it, it does not add to it.
|
|
1423
|
+
* Dropping the `targetType` half made every drag of a
|
|
1424
|
+
* card pan the canvas as well as move the card -- the
|
|
1425
|
+
* card ran at twice the pointer and the whole graph
|
|
1426
|
+
* slid underneath it, on a move whose RESULT was
|
|
1427
|
+
* right, because a pan writes nothing. */
|
|
1306
1428
|
{
|
|
1307
1429
|
type: "drag-canvas",
|
|
1308
1430
|
key: "drag-canvas",
|
|
1309
|
-
enable: (event) =>
|
|
1431
|
+
enable: (event) => {
|
|
1432
|
+
if(event.shiftKey) {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
if('targetType' in event) {
|
|
1436
|
+
return event.targetType === 'canvas';
|
|
1437
|
+
}
|
|
1438
|
+
return true; /* a key, not a pointer */
|
|
1439
|
+
},
|
|
1310
1440
|
},
|
|
1311
1441
|
"zoom-canvas",
|
|
1312
1442
|
/* Moves EVERY node in the `selected` state, not just
|
|
@@ -1321,7 +1451,12 @@ function configure_behaviour(gobj)
|
|
|
1321
1451
|
* ids travel with the event and the action does not
|
|
1322
1452
|
* have to race it. */
|
|
1323
1453
|
{
|
|
1324
|
-
|
|
1454
|
+
/* Ours: the built-in also rewrites the state of
|
|
1455
|
+
* EVERY element on a canvas click, behind the
|
|
1456
|
+
* gclass's back and outside its history pause --
|
|
1457
|
+
* a command that changed nothing, and a Save
|
|
1458
|
+
* button lit on a graph nobody had touched. */
|
|
1459
|
+
type: BRUSH_SELECT_OWNED,
|
|
1325
1460
|
key: "brush-select",
|
|
1326
1461
|
trigger: ["shift"],
|
|
1327
1462
|
enableElements: ["node"],
|
|
@@ -1343,6 +1478,16 @@ function configure_behaviour(gobj)
|
|
|
1343
1478
|
break;
|
|
1344
1479
|
case "operation":
|
|
1345
1480
|
priv.edit_mode = false;
|
|
1481
|
+
/* The camera is not an edit affordance: a mode that
|
|
1482
|
+
* cannot pan or zoom is a picture, and this one was one
|
|
1483
|
+
* -- `behaviors` was left empty here while `reading` and
|
|
1484
|
+
* `writing` next door both fill it. On a desktop the
|
|
1485
|
+
* toolbar still zooms and nothing pans; on a phone,
|
|
1486
|
+
* where the gestures ARE the camera, the graph froze. */
|
|
1487
|
+
behaviors = [
|
|
1488
|
+
"drag-canvas",
|
|
1489
|
+
"zoom-canvas",
|
|
1490
|
+
];
|
|
1346
1491
|
break;
|
|
1347
1492
|
default:
|
|
1348
1493
|
log_error(`operation mode unknown: ${operation_mode}`);
|
|
@@ -2252,6 +2397,42 @@ function history_resume(gobj)
|
|
|
2252
2397
|
gobj.priv._history_paused = false;
|
|
2253
2398
|
}
|
|
2254
2399
|
|
|
2400
|
+
/************************************************************
|
|
2401
|
+
* The history plugin exists while the graph is in EDITION,
|
|
2402
|
+
* and only there.
|
|
2403
|
+
*
|
|
2404
|
+
* It used to be installed at one MOMENT -- the arrival of the
|
|
2405
|
+
* last topic of the load -- and only if the graph happened to
|
|
2406
|
+
* be in edition right then. Entering edition afterwards, which
|
|
2407
|
+
* is the ordinary way in (the graph opens in reading and the
|
|
2408
|
+
* mode selector is next to it), left no history at all: Undo
|
|
2409
|
+
* and Redo were dead buttons that never even lit, `history_
|
|
2410
|
+
* pause()` had no `beforeAddCommand` to answer, and the Save
|
|
2411
|
+
* button was told the graph was dirty by `mark_graph_dirty()`
|
|
2412
|
+
* alone -- which is why a move offered Save without Undo.
|
|
2413
|
+
*
|
|
2414
|
+
* So it follows the MODE, and every place that changes the
|
|
2415
|
+
* mode calls this.
|
|
2416
|
+
************************************************************/
|
|
2417
|
+
function sync_history_plugin(gobj)
|
|
2418
|
+
{
|
|
2419
|
+
let priv = gobj.priv;
|
|
2420
|
+
|
|
2421
|
+
if(!priv.graph || !priv.graph_rendered) {
|
|
2422
|
+
/* Nothing to hang a plugin on yet: the load path calls
|
|
2423
|
+
* this again once the graph is drawn. */
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
if(priv.edit_mode) {
|
|
2428
|
+
graph_add_plugin(gobj, "history");
|
|
2429
|
+
} else {
|
|
2430
|
+
graph_remove_plugin(gobj, "history");
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
update_history_buttons(gobj);
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2255
2436
|
/************************************************************
|
|
2256
2437
|
*
|
|
2257
2438
|
************************************************************/
|
|
@@ -2657,6 +2838,7 @@ function graph_add_plugin(gobj, plugin_key, options)
|
|
|
2657
2838
|
let graph = priv.graph;
|
|
2658
2839
|
|
|
2659
2840
|
let plugin = graph_get_plugin(gobj, plugin_key);
|
|
2841
|
+
let existed = !!plugin;
|
|
2660
2842
|
if(!plugin) {
|
|
2661
2843
|
let plugin_def = {
|
|
2662
2844
|
key: plugin_key,
|
|
@@ -2672,7 +2854,11 @@ function graph_add_plugin(gobj, plugin_key, options)
|
|
|
2672
2854
|
|
|
2673
2855
|
switch(plugin_key) {
|
|
2674
2856
|
case "history":
|
|
2675
|
-
|
|
2857
|
+
/* Wired once per INSTANCE: this is called again on every
|
|
2858
|
+
* load and on every entry into edition, and a second
|
|
2859
|
+
* listener on the same emitter is a second repaint of the
|
|
2860
|
+
* same two buttons. */
|
|
2861
|
+
if(plugin && !existed) {
|
|
2676
2862
|
plugin.on(HistoryEvent.CHANGE, () => {
|
|
2677
2863
|
update_history_buttons(gobj);
|
|
2678
2864
|
});
|
|
@@ -2824,6 +3010,26 @@ const RESIZE_HANDLE_SIZE = 8;
|
|
|
2824
3010
|
const RESIZE_MIN_VP = 20;
|
|
2825
3011
|
const RESIZE_MIN_WORLD = 30;
|
|
2826
3012
|
|
|
3013
|
+
/*
|
|
3014
|
+
* A handle is TWO boxes: the one you see and the one you can
|
|
3015
|
+
* hit. They are the same box for a mouse, and on a touch
|
|
3016
|
+
* screen the hit box grows to a fingertip while the mark stays
|
|
3017
|
+
* a mark -- a 44px white square on each corner would hide the
|
|
3018
|
+
* node it is there to resize.
|
|
3019
|
+
*/
|
|
3020
|
+
const RESIZE_HANDLE_VISUAL_COARSE = 14;
|
|
3021
|
+
const RESIZE_HANDLE_HIT_COARSE = 44;
|
|
3022
|
+
|
|
3023
|
+
function resize_handle_visual()
|
|
3024
|
+
{
|
|
3025
|
+
return coarse_pointer()? RESIZE_HANDLE_VISUAL_COARSE: RESIZE_HANDLE_SIZE;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function resize_handle_hit()
|
|
3029
|
+
{
|
|
3030
|
+
return coarse_pointer()? RESIZE_HANDLE_HIT_COARSE: RESIZE_HANDLE_SIZE;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2827
3033
|
/************************************************************
|
|
2828
3034
|
* SVG icons for floating action buttons (28×28 circles).
|
|
2829
3035
|
* Each value is a raw SVG string sized 18×18 in a 24×24 viewBox.
|
|
@@ -2871,7 +3077,68 @@ const SVG_ICONS = {
|
|
|
2871
3077
|
};
|
|
2872
3078
|
|
|
2873
3079
|
/************************************************************
|
|
2874
|
-
*
|
|
3080
|
+
* Is the primary pointer a FINGER?
|
|
3081
|
+
*
|
|
3082
|
+
* Everything this gclass draws over the canvas is sized from
|
|
3083
|
+
* this answer. A mouse is a pixel and can hit an 8px square;
|
|
3084
|
+
* a fingertip covers about 40, and the overlay it lands on
|
|
3085
|
+
* decides between "open the properties" and "delete the node".
|
|
3086
|
+
*
|
|
3087
|
+
* Asked per call, not cached: a tablet with a keyboard dock
|
|
3088
|
+
* changes the answer without reloading the page.
|
|
3089
|
+
************************************************************/
|
|
3090
|
+
function coarse_pointer()
|
|
3091
|
+
{
|
|
3092
|
+
if(typeof window.matchMedia !== "function") {
|
|
3093
|
+
return false;
|
|
3094
|
+
}
|
|
3095
|
+
try {
|
|
3096
|
+
return window.matchMedia("(pointer: coarse)").matches === true;
|
|
3097
|
+
} catch(e) {
|
|
3098
|
+
return false;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
/************************************************************
|
|
3103
|
+
* The size of the round floating buttons (properties,
|
|
3104
|
+
* delete, link), and the GAP between two of them.
|
|
3105
|
+
*
|
|
3106
|
+
* The gap matters as much as the size: `node properties` and
|
|
3107
|
+
* `delete node` are drawn one under the other, and at 28px
|
|
3108
|
+
* with 4px between them a fingertip covers both -- with the
|
|
3109
|
+
* destructive one underneath.
|
|
3110
|
+
************************************************************/
|
|
3111
|
+
function floating_icon_size()
|
|
3112
|
+
{
|
|
3113
|
+
return coarse_pointer()? 44: 28;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
function floating_icon_gap()
|
|
3117
|
+
{
|
|
3118
|
+
return coarse_pointer()? 12: 4;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
/* Centre-to-top offset: the anchor names a point, the button
|
|
3122
|
+
* hangs centred on it. */
|
|
3123
|
+
function floating_icon_dy()
|
|
3124
|
+
{
|
|
3125
|
+
return -floating_icon_size() / 2;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
/* How far below the first button the second one starts. */
|
|
3129
|
+
function floating_icon_step()
|
|
3130
|
+
{
|
|
3131
|
+
return floating_icon_size() + floating_icon_gap();
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
/* Where a popover opens: clear of the button column. */
|
|
3135
|
+
function floating_popover_dx()
|
|
3136
|
+
{
|
|
3137
|
+
return 4 + floating_icon_size() + 4;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
/************************************************************
|
|
3141
|
+
* Create a floating circular icon button.
|
|
2875
3142
|
* @param {string} svgKey - key into SVG_ICONS
|
|
2876
3143
|
* @param {string} color - border & text color (e.g. '#1890ff')
|
|
2877
3144
|
* @param {number} left - CSS left in px
|
|
@@ -2882,19 +3149,34 @@ const SVG_ICONS = {
|
|
|
2882
3149
|
************************************************************/
|
|
2883
3150
|
function create_floating_icon(svgKey, color, left, top, title, onClick)
|
|
2884
3151
|
{
|
|
3152
|
+
const size = floating_icon_size();
|
|
2885
3153
|
const el = document.createElement('div');
|
|
2886
3154
|
el.title = title;
|
|
3155
|
+
el.setAttribute('aria-label', title);
|
|
2887
3156
|
el.innerHTML = SVG_ICONS[svgKey] || '';
|
|
2888
3157
|
el.style.cssText =
|
|
2889
3158
|
'position:absolute;' +
|
|
2890
3159
|
'left:' + left + 'px;' +
|
|
2891
3160
|
'top:' + top + 'px;' +
|
|
2892
|
-
'width:
|
|
3161
|
+
'width:' + size + 'px;height:' + size + 'px;' +
|
|
2893
3162
|
'display:flex;align-items:center;justify-content:center;' +
|
|
2894
3163
|
'background:#fff;border:1px solid ' + color + ';border-radius:50%;' +
|
|
2895
3164
|
'cursor:pointer;pointer-events:all;z-index:11;' +
|
|
3165
|
+
/* The button is a drag handle in one case (the link icon)
|
|
3166
|
+
* and the browser would claim that gesture as a scroll. */
|
|
3167
|
+
'touch-action:none;' +
|
|
2896
3168
|
'box-shadow:0 2px 6px rgba(0,0,0,0.15);' +
|
|
2897
3169
|
'color:' + color + ';';
|
|
3170
|
+
|
|
3171
|
+
/* The glyphs are authored 18×18; on the bigger button that
|
|
3172
|
+
* leaves a ring of empty white, so scale with the button. */
|
|
3173
|
+
const $svg = el.querySelector('svg');
|
|
3174
|
+
if($svg) {
|
|
3175
|
+
const glyph = Math.round(size * 0.5);
|
|
3176
|
+
$svg.setAttribute('width', String(glyph));
|
|
3177
|
+
$svg.setAttribute('height', String(glyph));
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2898
3180
|
el.addEventListener('click', (e) => {
|
|
2899
3181
|
e.stopPropagation();
|
|
2900
3182
|
onClick();
|
|
@@ -2954,7 +3236,7 @@ function show_dual_icons(gobj, x, y, gear_title, gear_click, trash_title, trash_
|
|
|
2954
3236
|
priv.$container.appendChild(icon_el);
|
|
2955
3237
|
|
|
2956
3238
|
const delete_el = create_floating_icon(
|
|
2957
|
-
'trash', '#ff4d4f', x, y +
|
|
3239
|
+
'trash', '#ff4d4f', x, y + floating_icon_step(), trash_title, trash_click
|
|
2958
3240
|
);
|
|
2959
3241
|
priv.$container.appendChild(delete_el);
|
|
2960
3242
|
|
|
@@ -2987,6 +3269,32 @@ function create_popover_base(left, top, className, borderColor, minWidth)
|
|
|
2987
3269
|
return popover;
|
|
2988
3270
|
}
|
|
2989
3271
|
|
|
3272
|
+
/************************************************************
|
|
3273
|
+
* Extra CSS every control inside a popover carries on a
|
|
3274
|
+
* touch screen.
|
|
3275
|
+
*
|
|
3276
|
+
* A popover is where the graph is TYPED into -- a colour, a
|
|
3277
|
+
* radius, an id -- and its controls were sized by their
|
|
3278
|
+
* padding alone, which lands them at 25-30px. A finger needs
|
|
3279
|
+
* 44, and the number is not ours: it is the floor both Apple
|
|
3280
|
+
* and Google publish, and the one every native control on the
|
|
3281
|
+
* device already meets.
|
|
3282
|
+
************************************************************/
|
|
3283
|
+
function touch_control_css()
|
|
3284
|
+
{
|
|
3285
|
+
if(!coarse_pointer()) {
|
|
3286
|
+
return '';
|
|
3287
|
+
}
|
|
3288
|
+
return 'min-height:44px;font-size:16px;';
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
/*
|
|
3292
|
+
* 16px is not a taste: iOS Safari ZOOMS the whole page when a
|
|
3293
|
+
* text field smaller than that takes focus, and the page never
|
|
3294
|
+
* zooms back -- the graph is left half off screen after typing
|
|
3295
|
+
* one id.
|
|
3296
|
+
*/
|
|
3297
|
+
|
|
2990
3298
|
function create_form_label(parent, text)
|
|
2991
3299
|
{
|
|
2992
3300
|
let label = document.createElement('label');
|
|
@@ -3004,7 +3312,7 @@ function create_form_color_input(parent, value, onInput)
|
|
|
3004
3312
|
input.style.cssText =
|
|
3005
3313
|
'width:100%;height:30px;padding:0;' +
|
|
3006
3314
|
'border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;' +
|
|
3007
|
-
'cursor:pointer;margin-bottom:10px;';
|
|
3315
|
+
'cursor:pointer;margin-bottom:10px;' + touch_control_css();
|
|
3008
3316
|
if(onInput) {
|
|
3009
3317
|
input.addEventListener('input', onInput);
|
|
3010
3318
|
}
|
|
@@ -3028,7 +3336,7 @@ function create_form_number_input(parent, value, min, max, onInput)
|
|
|
3028
3336
|
'background:var(--bulma-scheme-main-bis, #fff);' +
|
|
3029
3337
|
'color:var(--bulma-text-strong, #333);' +
|
|
3030
3338
|
'border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;' +
|
|
3031
|
-
'box-sizing:border-box;margin-bottom:10px;';
|
|
3339
|
+
'box-sizing:border-box;margin-bottom:10px;' + touch_control_css();
|
|
3032
3340
|
if(onInput) {
|
|
3033
3341
|
input.addEventListener('input', onInput);
|
|
3034
3342
|
}
|
|
@@ -3044,7 +3352,8 @@ function create_form_select(parent, options, marginBottom)
|
|
|
3044
3352
|
'background:var(--bulma-scheme-main-bis, #fff);' +
|
|
3045
3353
|
'color:var(--bulma-text-strong, #333);' +
|
|
3046
3354
|
'border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;' +
|
|
3047
|
-
'box-sizing:border-box;margin-bottom:' + (marginBottom || '12px') + ';'
|
|
3355
|
+
'box-sizing:border-box;margin-bottom:' + (marginBottom || '12px') + ';' +
|
|
3356
|
+
touch_control_css();
|
|
3048
3357
|
for(let opt of options) {
|
|
3049
3358
|
let o = document.createElement('option');
|
|
3050
3359
|
o.value = opt.value;
|
|
@@ -3068,7 +3377,7 @@ function create_form_button_row(parent, buttons)
|
|
|
3068
3377
|
for(let {text, style, onClick} of buttons) {
|
|
3069
3378
|
let btn = document.createElement('button');
|
|
3070
3379
|
btn.textContent = t(text);
|
|
3071
|
-
btn.style.cssText = style;
|
|
3380
|
+
btn.style.cssText = style + touch_control_css();
|
|
3072
3381
|
btn.addEventListener('click', (e) => {
|
|
3073
3382
|
e.stopPropagation();
|
|
3074
3383
|
onClick();
|
|
@@ -3322,10 +3631,10 @@ function toggle_in_selection(gobj, node_id)
|
|
|
3322
3631
|
{
|
|
3323
3632
|
let priv = gobj.priv;
|
|
3324
3633
|
|
|
3325
|
-
/* The union of the two views of the same thing
|
|
3326
|
-
* G6's state
|
|
3327
|
-
*
|
|
3328
|
-
* ring. */
|
|
3634
|
+
/* The union of the two views of the same thing: either can hold
|
|
3635
|
+
* an id the other lost -- G6's state is what a drag moves, the
|
|
3636
|
+
* painted set is what the reader can see -- and a card on screen
|
|
3637
|
+
* wearing a ring belongs to the selection whichever view has it. */
|
|
3329
3638
|
let current = new Set(selected_node_ids(gobj));
|
|
3330
3639
|
for(let id of (priv._selected_paint_ids || [])) {
|
|
3331
3640
|
current.add(id);
|
|
@@ -3448,7 +3757,7 @@ function show_resize_handles(gobj)
|
|
|
3448
3757
|
container.appendChild(selRect);
|
|
3449
3758
|
|
|
3450
3759
|
// 8 resize handles: nw, n, ne, w, e, sw, s, se
|
|
3451
|
-
|
|
3760
|
+
let handle_defs = [
|
|
3452
3761
|
{ cursor: 'nw-resize', mx: -1, my: -1 },
|
|
3453
3762
|
{ cursor: 'n-resize', mx: 0, my: -1 },
|
|
3454
3763
|
{ cursor: 'ne-resize', mx: 1, my: -1 },
|
|
@@ -3459,18 +3768,46 @@ function show_resize_handles(gobj)
|
|
|
3459
3768
|
{ cursor: 'se-resize', mx: 1, my: 1 },
|
|
3460
3769
|
];
|
|
3461
3770
|
|
|
3771
|
+
/*
|
|
3772
|
+
* On a touch screen, the CORNERS only. Eight fingertip-sized
|
|
3773
|
+
* hit boxes around a node 90px wide overlap into one blob, and
|
|
3774
|
+
* the edge handles are the ones that lose -- a corner resizes
|
|
3775
|
+
* both axes anyway, so nothing is out of reach.
|
|
3776
|
+
*/
|
|
3777
|
+
const hit = resize_handle_hit();
|
|
3778
|
+
const visual = resize_handle_visual();
|
|
3779
|
+
if(coarse_pointer()) {
|
|
3780
|
+
handle_defs = handle_defs.filter((def) => def.mx !== 0 && def.my !== 0);
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3462
3783
|
const handles = [];
|
|
3463
3784
|
for(const def of handle_defs) {
|
|
3785
|
+
/* The box that catches the finger: transparent, and never
|
|
3786
|
+
* in front of the mark of a NEIGHBOURING handle. */
|
|
3464
3787
|
const el = document.createElement('div');
|
|
3465
3788
|
el.style.cssText =
|
|
3466
3789
|
'position:absolute;' +
|
|
3467
|
-
'width:' +
|
|
3468
|
-
'height:' +
|
|
3469
|
-
'
|
|
3470
|
-
'border:1px solid #1890ff;' +
|
|
3790
|
+
'width:' + hit + 'px;' +
|
|
3791
|
+
'height:' + hit + 'px;' +
|
|
3792
|
+
'display:flex;align-items:center;justify-content:center;' +
|
|
3471
3793
|
'cursor:' + def.cursor + ';' +
|
|
3472
3794
|
'pointer-events:all;' +
|
|
3795
|
+
/* Without this the browser reads the drag as a scroll
|
|
3796
|
+
* and cancels the pointer mid-resize. */
|
|
3797
|
+
'touch-action:none;' +
|
|
3473
3798
|
'box-sizing:border-box;';
|
|
3799
|
+
|
|
3800
|
+
/* The box you see. */
|
|
3801
|
+
const mark = document.createElement('div');
|
|
3802
|
+
mark.style.cssText =
|
|
3803
|
+
'width:' + visual + 'px;' +
|
|
3804
|
+
'height:' + visual + 'px;' +
|
|
3805
|
+
'background:#fff;' +
|
|
3806
|
+
'border:1px solid #1890ff;' +
|
|
3807
|
+
'pointer-events:none;' +
|
|
3808
|
+
'box-sizing:border-box;';
|
|
3809
|
+
el.appendChild(mark);
|
|
3810
|
+
|
|
3474
3811
|
el.addEventListener('pointerdown', (e) => {
|
|
3475
3812
|
start_node_resize(gobj, e, def.mx, def.my);
|
|
3476
3813
|
});
|
|
@@ -3523,7 +3860,7 @@ function update_resize_handles_position(gobj)
|
|
|
3523
3860
|
function apply_handles_to_rect(gobj, rect)
|
|
3524
3861
|
{
|
|
3525
3862
|
let priv = gobj.priv;
|
|
3526
|
-
const HALF =
|
|
3863
|
+
const HALF = resize_handle_hit() / 2;
|
|
3527
3864
|
|
|
3528
3865
|
const left = rect.left;
|
|
3529
3866
|
const top = rect.top;
|
|
@@ -3544,23 +3881,15 @@ function apply_handles_to_rect(gobj, rect)
|
|
|
3544
3881
|
'box-sizing:border-box;';
|
|
3545
3882
|
}
|
|
3546
3883
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
{ x: left + w, y: top + h },
|
|
3557
|
-
];
|
|
3558
|
-
|
|
3559
|
-
for(let i = 0; i < priv._resize_handles.length; i++) {
|
|
3560
|
-
const handle = priv._resize_handles[i];
|
|
3561
|
-
const p = positions[i];
|
|
3562
|
-
handle.el.style.left = (p.x - HALF) + 'px';
|
|
3563
|
-
handle.el.style.top = (p.y - HALF) + 'px';
|
|
3884
|
+
/* Each handle carries the corner it is: -1 / 0 / +1 maps to
|
|
3885
|
+
* the near edge, the middle and the far edge. Read from the
|
|
3886
|
+
* handle instead of a parallel table, because the table is
|
|
3887
|
+
* not the same length on a touch screen. */
|
|
3888
|
+
for(const handle of priv._resize_handles) {
|
|
3889
|
+
const x = left + ((handle.mx + 1) / 2) * w;
|
|
3890
|
+
const y = top + ((handle.my + 1) / 2) * h;
|
|
3891
|
+
handle.el.style.left = (x - HALF) + 'px';
|
|
3892
|
+
handle.el.style.top = (y - HALF) + 'px';
|
|
3564
3893
|
}
|
|
3565
3894
|
}
|
|
3566
3895
|
|
|
@@ -3680,6 +4009,26 @@ function start_node_resize(gobj, e, mx, my)
|
|
|
3680
4009
|
************************************************************/
|
|
3681
4010
|
const PORT_HANDLE_SIZE = 8;
|
|
3682
4011
|
|
|
4012
|
+
/*
|
|
4013
|
+
* Same two-box idea as the node handles. Kept smaller than a
|
|
4014
|
+
* full 44: these four sit on the RIM of a port that is often a
|
|
4015
|
+
* 6px dot, so a fingertip-sized box on each would cover the
|
|
4016
|
+
* neighbouring ports as well -- and picking a port is how a
|
|
4017
|
+
* link is started.
|
|
4018
|
+
*/
|
|
4019
|
+
const PORT_HANDLE_VISUAL_COARSE = 12;
|
|
4020
|
+
const PORT_HANDLE_HIT_COARSE = 32;
|
|
4021
|
+
|
|
4022
|
+
function port_handle_visual()
|
|
4023
|
+
{
|
|
4024
|
+
return coarse_pointer()? PORT_HANDLE_VISUAL_COARSE: PORT_HANDLE_SIZE;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
function port_handle_hit()
|
|
4028
|
+
{
|
|
4029
|
+
return coarse_pointer()? PORT_HANDLE_HIT_COARSE: PORT_HANDLE_SIZE;
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3683
4032
|
/************************************************************
|
|
3684
4033
|
* Compute the canvas (world) position of a port on a node.
|
|
3685
4034
|
* Returns {x, y} in world coordinates.
|
|
@@ -3731,6 +4080,28 @@ function get_port_radius(gobj, node_id, port_key)
|
|
|
3731
4080
|
return style.portR || 6;
|
|
3732
4081
|
}
|
|
3733
4082
|
|
|
4083
|
+
/************************************************************
|
|
4084
|
+
* How much wider than the port itself its hit area is,
|
|
4085
|
+
* measured on the SCREEN and converted to world units.
|
|
4086
|
+
*
|
|
4087
|
+
* It used to be a flat `+4` in WORLD units, which is a
|
|
4088
|
+
* different target on every zoom: at the 50% a phone lands on
|
|
4089
|
+
* after fit, a 6-unit port plus 4 is a 5px radius -- one
|
|
4090
|
+
* fifth of a fingertip, and picking a port is the first step
|
|
4091
|
+
* of every link.
|
|
4092
|
+
************************************************************/
|
|
4093
|
+
function port_hit_slop_world(graph)
|
|
4094
|
+
{
|
|
4095
|
+
let slop = coarse_pointer()? 22: 6;
|
|
4096
|
+
let zoom = 1;
|
|
4097
|
+
try {
|
|
4098
|
+
zoom = graph.getZoom() || 1;
|
|
4099
|
+
} catch(e) {
|
|
4100
|
+
zoom = 1;
|
|
4101
|
+
}
|
|
4102
|
+
return slop / zoom;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
3734
4105
|
/************************************************************
|
|
3735
4106
|
* Detect if a click in canvas coordinates hits a port.
|
|
3736
4107
|
* Returns the port key string, or null if no port hit.
|
|
@@ -3749,6 +4120,7 @@ function detect_port_click(gobj, node_id, canvasX, canvasY)
|
|
|
3749
4120
|
|
|
3750
4121
|
let best_key = null;
|
|
3751
4122
|
let best_dist = Infinity;
|
|
4123
|
+
let slop = port_hit_slop_world(graph);
|
|
3752
4124
|
|
|
3753
4125
|
for(let i = 0; i < ports.length; i++) {
|
|
3754
4126
|
let pl = ports[i].placement || [0.5, 0.5];
|
|
@@ -3760,8 +4132,9 @@ function detect_port_click(gobj, node_id, canvasX, canvasY)
|
|
|
3760
4132
|
let dy = canvasY - py;
|
|
3761
4133
|
let dist = Math.sqrt(dx * dx + dy * dy);
|
|
3762
4134
|
|
|
3763
|
-
|
|
3764
|
-
|
|
4135
|
+
/* Nearest wins, so the enlarged areas of two ports
|
|
4136
|
+
* overlapping is not a tie -- it is the closer one. */
|
|
4137
|
+
if(dist <= r + slop && dist < best_dist) {
|
|
3765
4138
|
best_dist = dist;
|
|
3766
4139
|
best_key = ports[i].key;
|
|
3767
4140
|
}
|
|
@@ -3838,7 +4211,8 @@ function show_link_icon_if_fkey(gobj)
|
|
|
3838
4211
|
let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
|
|
3839
4212
|
|
|
3840
4213
|
let icon = create_floating_icon(
|
|
3841
|
-
'link', '#52c41a',
|
|
4214
|
+
'link', '#52c41a',
|
|
4215
|
+
vp[0] + floating_icon_size() / 2 + 4, vp[1] + floating_icon_dy(),
|
|
3842
4216
|
t('link to hook'), () => {
|
|
3843
4217
|
// Click (no drag) toggles linking mode
|
|
3844
4218
|
if(!priv._linking_mode) {
|
|
@@ -3877,8 +4251,9 @@ function update_link_icon_position(gobj)
|
|
|
3877
4251
|
return;
|
|
3878
4252
|
}
|
|
3879
4253
|
let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
|
|
3880
|
-
priv._link_icon_el.style.left =
|
|
3881
|
-
|
|
4254
|
+
priv._link_icon_el.style.left =
|
|
4255
|
+
(vp[0] + floating_icon_size() / 2 + 4) + 'px';
|
|
4256
|
+
priv._link_icon_el.style.top = (vp[1] + floating_icon_dy()) + 'px';
|
|
3882
4257
|
} catch(e) {
|
|
3883
4258
|
hide_link_icon(gobj);
|
|
3884
4259
|
}
|
|
@@ -4262,19 +4637,33 @@ function show_port_resize_handles(gobj)
|
|
|
4262
4637
|
{ cursor: 'w-resize', dx: -1, dy: 0 },
|
|
4263
4638
|
];
|
|
4264
4639
|
|
|
4640
|
+
const hit = port_handle_hit();
|
|
4641
|
+
const visual = port_handle_visual();
|
|
4642
|
+
|
|
4265
4643
|
const handles = [];
|
|
4266
4644
|
for(const def of handle_defs) {
|
|
4267
4645
|
const el = document.createElement('div');
|
|
4268
4646
|
el.style.cssText =
|
|
4269
4647
|
'position:absolute;' +
|
|
4270
|
-
'width:' +
|
|
4271
|
-
'height:' +
|
|
4648
|
+
'width:' + hit + 'px;' +
|
|
4649
|
+
'height:' + hit + 'px;' +
|
|
4650
|
+
'display:flex;align-items:center;justify-content:center;' +
|
|
4651
|
+
'cursor:' + def.cursor + ';' +
|
|
4652
|
+
'pointer-events:all;' +
|
|
4653
|
+
'touch-action:none;' +
|
|
4654
|
+
'box-sizing:border-box;';
|
|
4655
|
+
|
|
4656
|
+
const mark = document.createElement('div');
|
|
4657
|
+
mark.style.cssText =
|
|
4658
|
+
'width:' + visual + 'px;' +
|
|
4659
|
+
'height:' + visual + 'px;' +
|
|
4272
4660
|
'background:#fff;' +
|
|
4273
4661
|
'border:1px solid #fa8c16;' +
|
|
4274
4662
|
'border-radius:50%;' +
|
|
4275
|
-
'
|
|
4276
|
-
'pointer-events:all;' +
|
|
4663
|
+
'pointer-events:none;' +
|
|
4277
4664
|
'box-sizing:border-box;';
|
|
4665
|
+
el.appendChild(mark);
|
|
4666
|
+
|
|
4278
4667
|
el.addEventListener('pointerdown', (e) => {
|
|
4279
4668
|
start_port_resize(gobj, e);
|
|
4280
4669
|
});
|
|
@@ -4342,7 +4731,7 @@ function update_port_resize_handles_position(gobj)
|
|
|
4342
4731
|
function apply_port_handles(gobj, cx, cy, vpR)
|
|
4343
4732
|
{
|
|
4344
4733
|
let priv = gobj.priv;
|
|
4345
|
-
const HALF =
|
|
4734
|
+
const HALF = port_handle_hit() / 2;
|
|
4346
4735
|
|
|
4347
4736
|
// Dashed ring
|
|
4348
4737
|
const ring = priv._port_ring;
|
|
@@ -4534,7 +4923,7 @@ function show_edge_icon(gobj)
|
|
|
4534
4923
|
return;
|
|
4535
4924
|
}
|
|
4536
4925
|
|
|
4537
|
-
let icons = show_dual_icons(gobj, mid.x + 4, mid.y
|
|
4926
|
+
let icons = show_dual_icons(gobj, mid.x + 4, mid.y + floating_icon_dy(),
|
|
4538
4927
|
t('edge properties'), () => toggle_edge_popover(gobj),
|
|
4539
4928
|
t('unlink'), () => request_unlink_edge(gobj)
|
|
4540
4929
|
);
|
|
@@ -4562,16 +4951,18 @@ function update_edge_icon_position(gobj)
|
|
|
4562
4951
|
return;
|
|
4563
4952
|
}
|
|
4564
4953
|
priv._edge_icon_el.style.left = (mid.x + 4) + 'px';
|
|
4565
|
-
priv._edge_icon_el.style.top = (mid.y
|
|
4954
|
+
priv._edge_icon_el.style.top = (mid.y + floating_icon_dy()) + 'px';
|
|
4566
4955
|
if(priv._edge_delete_el) {
|
|
4567
4956
|
priv._edge_delete_el.style.left = (mid.x + 4) + 'px';
|
|
4568
|
-
priv._edge_delete_el.style.top =
|
|
4957
|
+
priv._edge_delete_el.style.top =
|
|
4958
|
+
(mid.y + floating_icon_dy() + floating_icon_step()) + 'px';
|
|
4569
4959
|
}
|
|
4570
4960
|
|
|
4571
4961
|
// Reposition popover if open
|
|
4572
4962
|
if(priv._edge_popover_el) {
|
|
4573
|
-
priv._edge_popover_el.style.left =
|
|
4574
|
-
|
|
4963
|
+
priv._edge_popover_el.style.left =
|
|
4964
|
+
(mid.x + floating_popover_dx()) + 'px';
|
|
4965
|
+
priv._edge_popover_el.style.top = (mid.y + floating_icon_dy()) + 'px';
|
|
4575
4966
|
clamp_popover_position(gobj, priv._edge_popover_el);
|
|
4576
4967
|
}
|
|
4577
4968
|
} catch(e) {
|
|
@@ -4631,7 +5022,10 @@ function show_edge_popover(gobj)
|
|
|
4631
5022
|
return;
|
|
4632
5023
|
}
|
|
4633
5024
|
|
|
4634
|
-
const popover = create_popover_base(
|
|
5025
|
+
const popover = create_popover_base(
|
|
5026
|
+
mid.x + floating_popover_dx(), mid.y + floating_icon_dy(),
|
|
5027
|
+
'g6-edge-popover', '#d9d9d9', 180
|
|
5028
|
+
);
|
|
4635
5029
|
|
|
4636
5030
|
// Live preview: update the selected edge in real time
|
|
4637
5031
|
function preview_edge() {
|
|
@@ -4747,7 +5141,7 @@ function show_node_icon(gobj)
|
|
|
4747
5141
|
return;
|
|
4748
5142
|
}
|
|
4749
5143
|
|
|
4750
|
-
let icons = show_dual_icons(gobj, rect.right + 4, rect.top
|
|
5144
|
+
let icons = show_dual_icons(gobj, rect.right + 4, rect.top + floating_icon_dy(),
|
|
4751
5145
|
t('node properties'), () => toggle_node_popover(gobj),
|
|
4752
5146
|
t('delete node'), () => request_delete_node(gobj)
|
|
4753
5147
|
);
|
|
@@ -4781,16 +5175,18 @@ function update_node_icon_position(gobj)
|
|
|
4781
5175
|
return;
|
|
4782
5176
|
}
|
|
4783
5177
|
priv._node_icon_el.style.left = (rect.right + 4) + 'px';
|
|
4784
|
-
priv._node_icon_el.style.top = (rect.top
|
|
5178
|
+
priv._node_icon_el.style.top = (rect.top + floating_icon_dy()) + 'px';
|
|
4785
5179
|
if(priv._node_delete_el) {
|
|
4786
5180
|
priv._node_delete_el.style.left = (rect.right + 4) + 'px';
|
|
4787
|
-
priv._node_delete_el.style.top =
|
|
5181
|
+
priv._node_delete_el.style.top =
|
|
5182
|
+
(rect.top + floating_icon_dy() + floating_icon_step()) + 'px';
|
|
4788
5183
|
}
|
|
4789
5184
|
|
|
4790
5185
|
// Reposition popover if open
|
|
4791
5186
|
if(priv._node_popover_el) {
|
|
4792
|
-
priv._node_popover_el.style.left =
|
|
4793
|
-
|
|
5187
|
+
priv._node_popover_el.style.left =
|
|
5188
|
+
(rect.right + floating_popover_dx()) + 'px';
|
|
5189
|
+
priv._node_popover_el.style.top = (rect.top + floating_icon_dy()) + 'px';
|
|
4794
5190
|
clamp_popover_position(gobj, priv._node_popover_el);
|
|
4795
5191
|
}
|
|
4796
5192
|
} catch(e) {
|
|
@@ -4843,7 +5239,10 @@ function show_node_popover(gobj)
|
|
|
4843
5239
|
return;
|
|
4844
5240
|
}
|
|
4845
5241
|
|
|
4846
|
-
const popover = create_popover_base(
|
|
5242
|
+
const popover = create_popover_base(
|
|
5243
|
+
rect.right + floating_popover_dx(), rect.top + floating_icon_dy(),
|
|
5244
|
+
'g6-node-popover', '#d9d9d9', 180
|
|
5245
|
+
);
|
|
4847
5246
|
|
|
4848
5247
|
let node_graph_type = nodeData.data && nodeData.data.desc ?
|
|
4849
5248
|
nodeData.data.desc.node_treedb_type : null;
|
|
@@ -5831,7 +6230,7 @@ function show_create_popover(gobj)
|
|
|
5831
6230
|
let topicSelect = document.createElement('select');
|
|
5832
6231
|
topicSelect.style.cssText =
|
|
5833
6232
|
'width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;' +
|
|
5834
|
-
'font-size:13px;margin-bottom:8px;';
|
|
6233
|
+
'font-size:13px;margin-bottom:8px;' + touch_control_css();
|
|
5835
6234
|
for(let i = 0; i < topics.length; i++) {
|
|
5836
6235
|
let opt = document.createElement('option');
|
|
5837
6236
|
opt.value = topics[i].topic_name;
|
|
@@ -5874,7 +6273,8 @@ function show_create_popover(gobj)
|
|
|
5874
6273
|
idInput.placeholder = t('node id');
|
|
5875
6274
|
idInput.style.cssText =
|
|
5876
6275
|
'width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;' +
|
|
5877
|
-
'font-size:13px;margin-bottom:10px;box-sizing:border-box;'
|
|
6276
|
+
'font-size:13px;margin-bottom:10px;box-sizing:border-box;' +
|
|
6277
|
+
touch_control_css();
|
|
5878
6278
|
popover.appendChild(idInput);
|
|
5879
6279
|
|
|
5880
6280
|
// Error message area
|
|
@@ -6668,11 +7068,7 @@ function ac_load_data(gobj, event, kw, src)
|
|
|
6668
7068
|
log_error(`${gobj_short_name(gobj)}: cannot fit the graph: ${e}`);
|
|
6669
7069
|
});
|
|
6670
7070
|
}
|
|
6671
|
-
|
|
6672
|
-
graph_add_plugin(gobj, "history");
|
|
6673
|
-
} else {
|
|
6674
|
-
graph_remove_plugin(gobj, "history");
|
|
6675
|
-
}
|
|
7071
|
+
sync_history_plugin(gobj);
|
|
6676
7072
|
/* Nodes exist and are positioned now: apply a focus
|
|
6677
7073
|
* requested before the data was ready (deep link). */
|
|
6678
7074
|
if(priv._pending_focus_topic !== null) {
|
|
@@ -6960,6 +7356,16 @@ function ac_resize(gobj, event, kw, src)
|
|
|
6960
7356
|
}
|
|
6961
7357
|
}
|
|
6962
7358
|
|
|
7359
|
+
/* Crossing the width where the toolbars start folding changes
|
|
7360
|
+
* what they contain -- a fold button appears or goes away --
|
|
7361
|
+
* and nothing else would ever rebuild them. Only on the
|
|
7362
|
+
* CROSSING: a resize fires on every frame of a drag. */
|
|
7363
|
+
let can_collapse = toolbars_can_collapse(gobj);
|
|
7364
|
+
if(can_collapse !== priv._toolbars_could_collapse) {
|
|
7365
|
+
priv._toolbars_could_collapse = can_collapse;
|
|
7366
|
+
update_toolbar(gobj);
|
|
7367
|
+
}
|
|
7368
|
+
|
|
6963
7369
|
return 0;
|
|
6964
7370
|
}
|
|
6965
7371
|
|
|
@@ -7018,6 +7424,22 @@ function ac_zoom_reset(gobj, event, kw, src)
|
|
|
7018
7424
|
return 0;
|
|
7019
7425
|
}
|
|
7020
7426
|
|
|
7427
|
+
/************************************************************
|
|
7428
|
+
* Fold / unfold the floating toolbars.
|
|
7429
|
+
*
|
|
7430
|
+
* One flag for BOTH: they are one wall over the canvas, and
|
|
7431
|
+
* the button that unfolds them has to be the same button
|
|
7432
|
+
* whichever edge the user reaches for.
|
|
7433
|
+
************************************************************/
|
|
7434
|
+
function ac_toggle_toolbars(gobj, event, kw, src)
|
|
7435
|
+
{
|
|
7436
|
+
let priv = gobj.priv;
|
|
7437
|
+
|
|
7438
|
+
priv.toolbar_collapsed = !toolbars_collapsed(gobj);
|
|
7439
|
+
update_toolbar(gobj);
|
|
7440
|
+
return 0;
|
|
7441
|
+
}
|
|
7442
|
+
|
|
7021
7443
|
function ac_auto_fit(gobj, event, kw, src)
|
|
7022
7444
|
{
|
|
7023
7445
|
graph_fitview(gobj);
|
|
@@ -7104,6 +7526,10 @@ function ac_set_operation_mode(gobj, event, kw, src)
|
|
|
7104
7526
|
gobj_write_attr(gobj, "operation_mode", kw.operation_mode);
|
|
7105
7527
|
configure_behaviour(gobj);
|
|
7106
7528
|
update_toolbar(gobj);
|
|
7529
|
+
|
|
7530
|
+
/* `configure_behaviour` is what decides `edit_mode`, and the
|
|
7531
|
+
* history plugin belongs to that decision. */
|
|
7532
|
+
sync_history_plugin(gobj);
|
|
7107
7533
|
return 0;
|
|
7108
7534
|
}
|
|
7109
7535
|
|
|
@@ -7506,6 +7932,7 @@ function create_gclass(gclass_name)
|
|
|
7506
7932
|
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
7507
7933
|
["EV_ZOOM_OUT", ac_zoom_out, null],
|
|
7508
7934
|
["EV_ZOOM_RESET", ac_zoom_reset, null],
|
|
7935
|
+
["EV_TOGGLE_TOOLBARS", ac_toggle_toolbars, null],
|
|
7509
7936
|
["EV_AUTO_FIT", ac_auto_fit, null],
|
|
7510
7937
|
["EV_ZOOM_SELECTION", ac_zoom_selection, null],
|
|
7511
7938
|
["EV_FOCUS_TOPIC", ac_focus_topic, null],
|
|
@@ -7553,6 +7980,7 @@ function create_gclass(gclass_name)
|
|
|
7553
7980
|
["EV_ZOOM_IN", 0],
|
|
7554
7981
|
["EV_ZOOM_OUT", 0],
|
|
7555
7982
|
["EV_ZOOM_RESET", 0],
|
|
7983
|
+
["EV_TOGGLE_TOOLBARS", 0],
|
|
7556
7984
|
["EV_AUTO_FIT", 0],
|
|
7557
7985
|
["EV_ZOOM_SELECTION", 0],
|
|
7558
7986
|
["EV_FOCUS_TOPIC", 0],
|