@yuneta/gobj-ui 7.23.13 → 7.23.15
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 +26 -3
- package/dist/gobj-ui.cjs.js +121 -33
- package/dist/gobj-ui.es.js +121 -33
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +25 -4
- package/src/g6_touch_gestures.js +175 -55
- package/src/lib_graph.css +34 -0
- package/src/press_arbiter.js +84 -0
- package/src/press_arbiter.test.js +63 -0
package/README.md
CHANGED
|
@@ -854,6 +854,29 @@ structural rather than cosmetic — and none of them is visible in the CSS:
|
|
|
854
854
|
slop) that re-emits G6's own forwarded event under the name the plugin
|
|
855
855
|
listens for — so `getItems(e)` sees exactly what a right click gives it,
|
|
856
856
|
including the port under the finger.
|
|
857
|
+
- **One finger has to serve three commands, so the press is arbitrated — at
|
|
858
|
+
the RELEASE** (`press_arbiter.js`, pure and tested): moved → drag; still and
|
|
859
|
+
let go quickly → the element's own action; still and held past 500ms → the
|
|
860
|
+
context menu — with a 15ms haptic tick at the 500ms mark, which is a NOTICE
|
|
861
|
+
and not the decision (a finger that buzzes and then carries the node away
|
|
862
|
+
still gets its drag). Firing the menu on a TIMER instead cannot arbitrate
|
|
863
|
+
anything,
|
|
864
|
+
because at the moment it fires the gesture is not over: it opened while
|
|
865
|
+
`drag-element` was already carrying the node, so one press meant both things
|
|
866
|
+
and the menu sat over a card running away underneath it. Two more things the
|
|
867
|
+
same press was doing, both fixed with it: `click` is not taken from the DOM
|
|
868
|
+
either (`@antv/g`'s `onPointerUp` synthesises one), so the press that opened
|
|
869
|
+
the menu went on to click the node it opened the menu on — the click
|
|
870
|
+
handlers ask `consume_long_press_click()` first; and the BROWSER's own menu,
|
|
871
|
+
which opens while the finger is still down, is refused for as long as there
|
|
872
|
+
is a finger on the glass.
|
|
873
|
+
- **The browser was taking the gesture outright.** G6 puts `touch-action:
|
|
874
|
+
none` on its canvas and nothing on its HTML nodes, which are ordinary DIVs
|
|
875
|
+
over it — so a drag that started on a **card** was a page scroll: two
|
|
876
|
+
`pointermove`s through, then `pointercancel`, and the node stopped dead
|
|
877
|
+
about 20px in while the page slid. `.graph-container` refuses those gestures
|
|
878
|
+
whole now; the panels and the context menu keep `touch-action: auto`, so a
|
|
879
|
+
finger still scrolls what is meant to scroll.
|
|
857
880
|
|
|
858
881
|
Everything a finger has to LAND on is sized off `(pointer: coarse)`, which
|
|
859
882
|
means **a mouse sees no change at all**: node resize handles become a 14px
|
|
@@ -884,9 +907,9 @@ gesture to give them: G6 binds panning and the band to the same plain drag, so
|
|
|
884
907
|
one of the two has to stand aside. The edit toolbar carries a **selection
|
|
885
908
|
mode** toggle (the dashed marquee, next to `+`): while it is on, a tap picks a
|
|
886
909
|
card and a drag on the background draws the band, and panning is what stands
|
|
887
|
-
aside. It looks **pressed** while it is on (`pressed_state`, an
|
|
888
|
-
neutral chip) rather than taking one of the palette's colours: each of
|
|
889
|
-
names a KIND of action, and `7.23.11` borrowed undo/redo's violet for it —
|
|
910
|
+
aside. It looks **pressed** while it is on (`7.23.12`, `pressed_state`, an
|
|
911
|
+
inverted neutral chip) rather than taking one of the palette's colours: each of
|
|
912
|
+
those names a KIND of action, and `7.23.11` borrowed undo/redo's violet for it —
|
|
890
913
|
which put the same colour on two neighbouring buttons for two different
|
|
891
914
|
reasons, and carried the whole state change in the hairline of an outline
|
|
892
915
|
glyph. It is deliberately a button and not a heuristic — the toolbar says which
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -9503,6 +9503,29 @@ function yui_graph_fold_items(gobj, wide) {
|
|
|
9503
9503
|
function yui_graph_refresh_item(gobj, wide) {
|
|
9504
9504
|
return camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", wide);
|
|
9505
9505
|
}
|
|
9506
|
+
/***************************************************************
|
|
9507
|
+
* Arbitration
|
|
9508
|
+
***************************************************************/
|
|
9509
|
+
/************************************************************
|
|
9510
|
+
* Did the finger travel far enough to mean a DRAG?
|
|
9511
|
+
*
|
|
9512
|
+
* `press` is what the pointerdown recorded: {x, y, t0}.
|
|
9513
|
+
************************************************************/
|
|
9514
|
+
function press_moved(press, x, y) {
|
|
9515
|
+
if (!press) return false;
|
|
9516
|
+
return Math.abs(x - press.x) > 10 || Math.abs(y - press.y) > 10;
|
|
9517
|
+
}
|
|
9518
|
+
/************************************************************
|
|
9519
|
+
* What the press meant, asked when the finger comes up.
|
|
9520
|
+
*
|
|
9521
|
+
* Answers "none" when there was no press to arbitrate.
|
|
9522
|
+
************************************************************/
|
|
9523
|
+
function classify_press(press, x, y, t) {
|
|
9524
|
+
if (!press) return "none";
|
|
9525
|
+
if (press_moved(press, x, y)) return "drag";
|
|
9526
|
+
if (t - press.t0 >= 500) return "long";
|
|
9527
|
+
return "tap";
|
|
9528
|
+
}
|
|
9506
9529
|
//#endregion
|
|
9507
9530
|
//#region src/g6_touch_gestures.js
|
|
9508
9531
|
/***********************************************************************
|
|
@@ -9539,6 +9562,21 @@ function yui_graph_refresh_item(gobj, wide) {
|
|
|
9539
9562
|
* forwarded event object -- which is why the plugin's
|
|
9540
9563
|
* `getItems(e)` receives exactly the shape it expects.
|
|
9541
9564
|
*
|
|
9565
|
+
* ONE finger has to serve three commands -- move the
|
|
9566
|
+
* node, act on it, open its menu -- so the press is
|
|
9567
|
+
* arbitrated, and arbitrated at the RELEASE:
|
|
9568
|
+
*
|
|
9569
|
+
* moved -> drag
|
|
9570
|
+
* still, let go fast -> the element's own action
|
|
9571
|
+
* still, held >= 500ms -> the context menu
|
|
9572
|
+
*
|
|
9573
|
+
* A menu opened on a TIMER instead cannot be arbitrated
|
|
9574
|
+
* at all: it opens while `drag-element` is already
|
|
9575
|
+
* carrying the node, so the same press means both things
|
|
9576
|
+
* and the user gets a menu over a card running away
|
|
9577
|
+
* underneath it. `classify_press` is where the three
|
|
9578
|
+
* live, and it is pure.
|
|
9579
|
+
*
|
|
9542
9580
|
* Both are built on ONE per-graph record of the fingers
|
|
9543
9581
|
* on the glass, read from the NATIVE touch events -- see
|
|
9544
9582
|
* `touch_state_of` for why the pointer stream, which looks
|
|
@@ -9551,11 +9589,6 @@ function yui_graph_refresh_item(gobj, wide) {
|
|
|
9551
9589
|
* All Rights Reserved.
|
|
9552
9590
|
***********************************************************************/
|
|
9553
9591
|
/***************************************************************
|
|
9554
|
-
* Constants
|
|
9555
|
-
***************************************************************/
|
|
9556
|
-
var LONG_PRESS_MS = 500;
|
|
9557
|
-
var LONG_PRESS_SLOP = 10;
|
|
9558
|
-
/***************************************************************
|
|
9559
9592
|
* Per-graph record of the fingers on the glass
|
|
9560
9593
|
***************************************************************/
|
|
9561
9594
|
var __touch_state__ = /* @__PURE__ */ new WeakMap();
|
|
@@ -9582,7 +9615,9 @@ function touch_state_of(graph) {
|
|
|
9582
9615
|
if (state) return state;
|
|
9583
9616
|
state = {
|
|
9584
9617
|
count: 0,
|
|
9585
|
-
|
|
9618
|
+
multi: false,
|
|
9619
|
+
container: null,
|
|
9620
|
+
long_press_click: false
|
|
9586
9621
|
};
|
|
9587
9622
|
__touch_state__.set(graph, state);
|
|
9588
9623
|
let container = graph.getCanvas?.()?.getContainer?.();
|
|
@@ -9590,11 +9625,16 @@ function touch_state_of(graph) {
|
|
|
9590
9625
|
state.container = container;
|
|
9591
9626
|
let read = (event) => {
|
|
9592
9627
|
state.count = event.touches ? event.touches.length : 0;
|
|
9628
|
+
if (state.count >= 2) state.multi = true;
|
|
9629
|
+
else if (state.count === 0) state.multi = false;
|
|
9593
9630
|
};
|
|
9594
9631
|
container.addEventListener("touchstart", read, { passive: true });
|
|
9595
9632
|
container.addEventListener("touchmove", read, { passive: true });
|
|
9596
9633
|
container.addEventListener("touchend", read, { passive: true });
|
|
9597
9634
|
container.addEventListener("touchcancel", read, { passive: true });
|
|
9635
|
+
container.addEventListener("contextmenu", (event) => {
|
|
9636
|
+
if (state.count > 0) event.preventDefault();
|
|
9637
|
+
}, { capture: true });
|
|
9598
9638
|
return state;
|
|
9599
9639
|
}
|
|
9600
9640
|
/************************************************************
|
|
@@ -9695,6 +9735,24 @@ function ensure_pinch_zoom_patch() {
|
|
|
9695
9735
|
* Long press
|
|
9696
9736
|
***************************/
|
|
9697
9737
|
/************************************************************
|
|
9738
|
+
* Is the click being dispatched right now the tail of a long
|
|
9739
|
+
* press? Answers true ONCE, to the first asker.
|
|
9740
|
+
*
|
|
9741
|
+
* @antv/g does not take `click` from the DOM either: it makes
|
|
9742
|
+
* one out of the pointerdown/pointerup pair itself, inside
|
|
9743
|
+
* the same `onPointerUp` that just fed us the release. So
|
|
9744
|
+
* the press that opened the menu goes on to click whatever it
|
|
9745
|
+
* opened the menu ON -- selecting the node, or worse, dropping
|
|
9746
|
+
* a selection of several -- unless the handler asks.
|
|
9747
|
+
************************************************************/
|
|
9748
|
+
function consume_long_press_click(graph) {
|
|
9749
|
+
if (!graph) return false;
|
|
9750
|
+
let state = __touch_state__.get(graph);
|
|
9751
|
+
if (!state || !state.long_press_click) return false;
|
|
9752
|
+
state.long_press_click = false;
|
|
9753
|
+
return true;
|
|
9754
|
+
}
|
|
9755
|
+
/************************************************************
|
|
9698
9756
|
* Make a long press open the context menu.
|
|
9699
9757
|
*
|
|
9700
9758
|
* Returns the function that uninstalls it.
|
|
@@ -9704,19 +9762,34 @@ function ensure_pinch_zoom_patch() {
|
|
|
9704
9762
|
* the name the plugin listens for, so `getItems(e)` and
|
|
9705
9763
|
* `enable(e)` see the same object a right-click would give
|
|
9706
9764
|
* them -- same `target`, same `targetType`, same `client`.
|
|
9765
|
+
*
|
|
9766
|
+
* `options.enable(event)` decides, at the DOWN, whether this
|
|
9767
|
+
* press is even a candidate. Pass the plugin's own predicate:
|
|
9768
|
+
* a press on the empty canvas would otherwise arm a menu the
|
|
9769
|
+
* plugin then refuses to show, and swallow the click that
|
|
9770
|
+
* would have cleared the selection.
|
|
9707
9771
|
************************************************************/
|
|
9708
|
-
function install_long_press_contextmenu(graph) {
|
|
9772
|
+
function install_long_press_contextmenu(graph, options) {
|
|
9709
9773
|
if (!graph) return () => {};
|
|
9774
|
+
let enable = options?.enable;
|
|
9710
9775
|
let state = touch_state_of(graph);
|
|
9711
9776
|
let pending = null;
|
|
9712
|
-
let
|
|
9777
|
+
let disarm = () => {
|
|
9713
9778
|
if (pending) {
|
|
9714
|
-
clearTimeout(pending.
|
|
9779
|
+
clearTimeout(pending.tick);
|
|
9715
9780
|
pending = null;
|
|
9716
9781
|
}
|
|
9717
9782
|
};
|
|
9783
|
+
let announce = () => {
|
|
9784
|
+
if (!pending) return;
|
|
9785
|
+
if (state.multi) return;
|
|
9786
|
+
navigator.vibrate?.(15);
|
|
9787
|
+
};
|
|
9718
9788
|
let fire = (event) => {
|
|
9719
|
-
|
|
9789
|
+
state.long_press_click = true;
|
|
9790
|
+
setTimeout(() => {
|
|
9791
|
+
state.long_press_click = false;
|
|
9792
|
+
}, 0);
|
|
9720
9793
|
let swallow = (evt) => {
|
|
9721
9794
|
evt.stopPropagation();
|
|
9722
9795
|
document.removeEventListener("click", swallow, true);
|
|
@@ -9725,51 +9798,62 @@ function install_long_press_contextmenu(graph) {
|
|
|
9725
9798
|
setTimeout(() => {
|
|
9726
9799
|
document.removeEventListener("click", swallow, true);
|
|
9727
9800
|
}, 1e3);
|
|
9728
|
-
let container = graph.getCanvas?.()?.getContainer?.();
|
|
9729
|
-
if (container) container.addEventListener("contextmenu", (evt) => {
|
|
9730
|
-
evt.preventDefault();
|
|
9731
|
-
}, {
|
|
9732
|
-
once: true,
|
|
9733
|
-
capture: true
|
|
9734
|
-
});
|
|
9735
9801
|
graph.emit(`${event.targetType}:${_antv_g6.CommonEvent.CONTEXT_MENU}`, event);
|
|
9736
9802
|
graph.emit(_antv_g6.CommonEvent.CONTEXT_MENU, event);
|
|
9737
9803
|
};
|
|
9738
9804
|
let on_down = (event) => {
|
|
9739
|
-
|
|
9805
|
+
disarm();
|
|
9740
9806
|
if (event.pointerType !== "touch") return;
|
|
9741
|
-
if (state.count > 1) return;
|
|
9807
|
+
if (state.count > 1 || state.multi) return;
|
|
9808
|
+
if (enable && !enable(event)) return;
|
|
9742
9809
|
let x = event.client?.x;
|
|
9743
9810
|
let y = event.client?.y;
|
|
9744
9811
|
if (x === void 0 || y === void 0) return;
|
|
9745
|
-
let frozen = Object.assign({}, event);
|
|
9746
9812
|
pending = {
|
|
9747
9813
|
x,
|
|
9748
9814
|
y,
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9815
|
+
t0: Date.now(),
|
|
9816
|
+
event: Object.assign({}, event),
|
|
9817
|
+
tick: setTimeout(announce, 500)
|
|
9752
9818
|
};
|
|
9753
9819
|
};
|
|
9754
9820
|
let on_move = (event) => {
|
|
9755
9821
|
if (!pending) return;
|
|
9756
|
-
if (state.count > 1) {
|
|
9757
|
-
|
|
9822
|
+
if (state.count > 1 || state.multi) {
|
|
9823
|
+
disarm();
|
|
9758
9824
|
return;
|
|
9759
9825
|
}
|
|
9760
9826
|
let x = event.client?.x;
|
|
9761
9827
|
let y = event.client?.y;
|
|
9762
9828
|
if (x === void 0 || y === void 0) return;
|
|
9763
|
-
if (
|
|
9829
|
+
if (press_moved(pending, x, y)) disarm();
|
|
9764
9830
|
};
|
|
9831
|
+
let on_up = (event) => {
|
|
9832
|
+
let press = pending;
|
|
9833
|
+
if (press) clearTimeout(press.tick);
|
|
9834
|
+
pending = null;
|
|
9835
|
+
if (!press) return;
|
|
9836
|
+
if (state.multi) return;
|
|
9837
|
+
let x = event.client?.x;
|
|
9838
|
+
let y = event.client?.y;
|
|
9839
|
+
if (x === void 0) x = press.x;
|
|
9840
|
+
if (y === void 0) y = press.y;
|
|
9841
|
+
if (classify_press(press, x, y, Date.now()) !== "long") return;
|
|
9842
|
+
fire(press.event);
|
|
9843
|
+
};
|
|
9844
|
+
let on_touch_cancel = () => {
|
|
9845
|
+
disarm();
|
|
9846
|
+
};
|
|
9847
|
+
state.container?.addEventListener("touchcancel", on_touch_cancel);
|
|
9765
9848
|
graph.on(_antv_g6.CommonEvent.POINTER_DOWN, on_down);
|
|
9766
9849
|
graph.on(_antv_g6.CommonEvent.POINTER_MOVE, on_move);
|
|
9767
|
-
graph.on(_antv_g6.CommonEvent.POINTER_UP,
|
|
9850
|
+
graph.on(_antv_g6.CommonEvent.POINTER_UP, on_up);
|
|
9768
9851
|
return () => {
|
|
9769
|
-
|
|
9852
|
+
disarm();
|
|
9853
|
+
state.container?.removeEventListener("touchcancel", on_touch_cancel);
|
|
9770
9854
|
graph.off(_antv_g6.CommonEvent.POINTER_DOWN, on_down);
|
|
9771
9855
|
graph.off(_antv_g6.CommonEvent.POINTER_MOVE, on_move);
|
|
9772
|
-
graph.off(_antv_g6.CommonEvent.POINTER_UP,
|
|
9856
|
+
graph.off(_antv_g6.CommonEvent.POINTER_UP, on_up);
|
|
9773
9857
|
};
|
|
9774
9858
|
}
|
|
9775
9859
|
//#endregion
|
|
@@ -47977,6 +48061,7 @@ function configure_events(gobj) {
|
|
|
47977
48061
|
let priv = gobj.priv;
|
|
47978
48062
|
let graph = priv.graph;
|
|
47979
48063
|
graph.on(_antv_g6.CanvasEvent.CLICK, (evt) => {
|
|
48064
|
+
if (consume_long_press_click(graph)) return;
|
|
47980
48065
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CANVAS_CLICK", { evt }, gobj);
|
|
47981
48066
|
});
|
|
47982
48067
|
graph.on(_antv_g6.NodeEvent.DRAG, (evt) => {
|
|
@@ -47990,12 +48075,14 @@ function configure_events(gobj) {
|
|
|
47990
48075
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_DRAG_END", { evt }, gobj);
|
|
47991
48076
|
});
|
|
47992
48077
|
graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
|
|
48078
|
+
if (consume_long_press_click(graph)) return;
|
|
47993
48079
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
|
|
47994
48080
|
});
|
|
47995
48081
|
graph.on(_antv_g6.NodeEvent.CONTEXT_MENU, (evt) => {
|
|
47996
48082
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CONTEXT_MENU", { evt }, gobj);
|
|
47997
48083
|
});
|
|
47998
48084
|
graph.on(_antv_g6.EdgeEvent.CLICK, (evt) => {
|
|
48085
|
+
if (consume_long_press_click(graph)) return;
|
|
47999
48086
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_EDGE_CLICK", { evt }, gobj);
|
|
48000
48087
|
});
|
|
48001
48088
|
graph.on("aftertransform", () => {
|
|
@@ -48059,6 +48146,9 @@ function configure_plugins(gobj) {
|
|
|
48059
48146
|
update_toolbar(gobj);
|
|
48060
48147
|
}
|
|
48061
48148
|
});
|
|
48149
|
+
let menu_enable = (e) => {
|
|
48150
|
+
return e.targetType === "node" || e.targetType === "edge";
|
|
48151
|
+
};
|
|
48062
48152
|
graph_add_plugin(gobj, "contextmenu", {
|
|
48063
48153
|
trigger: "contextmenu",
|
|
48064
48154
|
onClick: (value) => {
|
|
@@ -48067,12 +48157,10 @@ function configure_plugins(gobj) {
|
|
|
48067
48157
|
getItems: (e) => {
|
|
48068
48158
|
return build_context_menu_items(gobj, e);
|
|
48069
48159
|
},
|
|
48070
|
-
enable:
|
|
48071
|
-
return e.targetType === "node" || e.targetType === "edge";
|
|
48072
|
-
}
|
|
48160
|
+
enable: menu_enable
|
|
48073
48161
|
});
|
|
48074
48162
|
if (priv._uninstall_long_press) priv._uninstall_long_press();
|
|
48075
|
-
priv._uninstall_long_press = install_long_press_contextmenu(priv.graph);
|
|
48163
|
+
priv._uninstall_long_press = install_long_press_contextmenu(priv.graph, { enable: menu_enable });
|
|
48076
48164
|
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_toolbar")) {
|
|
48077
48165
|
configure_toolbar(gobj);
|
|
48078
48166
|
configure_toolbar_edit(gobj);
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -9498,6 +9498,29 @@ function yui_graph_fold_items(gobj, wide) {
|
|
|
9498
9498
|
function yui_graph_refresh_item(gobj, wide) {
|
|
9499
9499
|
return camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", wide);
|
|
9500
9500
|
}
|
|
9501
|
+
/***************************************************************
|
|
9502
|
+
* Arbitration
|
|
9503
|
+
***************************************************************/
|
|
9504
|
+
/************************************************************
|
|
9505
|
+
* Did the finger travel far enough to mean a DRAG?
|
|
9506
|
+
*
|
|
9507
|
+
* `press` is what the pointerdown recorded: {x, y, t0}.
|
|
9508
|
+
************************************************************/
|
|
9509
|
+
function press_moved(press, x, y) {
|
|
9510
|
+
if (!press) return false;
|
|
9511
|
+
return Math.abs(x - press.x) > 10 || Math.abs(y - press.y) > 10;
|
|
9512
|
+
}
|
|
9513
|
+
/************************************************************
|
|
9514
|
+
* What the press meant, asked when the finger comes up.
|
|
9515
|
+
*
|
|
9516
|
+
* Answers "none" when there was no press to arbitrate.
|
|
9517
|
+
************************************************************/
|
|
9518
|
+
function classify_press(press, x, y, t) {
|
|
9519
|
+
if (!press) return "none";
|
|
9520
|
+
if (press_moved(press, x, y)) return "drag";
|
|
9521
|
+
if (t - press.t0 >= 500) return "long";
|
|
9522
|
+
return "tap";
|
|
9523
|
+
}
|
|
9501
9524
|
//#endregion
|
|
9502
9525
|
//#region src/g6_touch_gestures.js
|
|
9503
9526
|
/***********************************************************************
|
|
@@ -9534,6 +9557,21 @@ function yui_graph_refresh_item(gobj, wide) {
|
|
|
9534
9557
|
* forwarded event object -- which is why the plugin's
|
|
9535
9558
|
* `getItems(e)` receives exactly the shape it expects.
|
|
9536
9559
|
*
|
|
9560
|
+
* ONE finger has to serve three commands -- move the
|
|
9561
|
+
* node, act on it, open its menu -- so the press is
|
|
9562
|
+
* arbitrated, and arbitrated at the RELEASE:
|
|
9563
|
+
*
|
|
9564
|
+
* moved -> drag
|
|
9565
|
+
* still, let go fast -> the element's own action
|
|
9566
|
+
* still, held >= 500ms -> the context menu
|
|
9567
|
+
*
|
|
9568
|
+
* A menu opened on a TIMER instead cannot be arbitrated
|
|
9569
|
+
* at all: it opens while `drag-element` is already
|
|
9570
|
+
* carrying the node, so the same press means both things
|
|
9571
|
+
* and the user gets a menu over a card running away
|
|
9572
|
+
* underneath it. `classify_press` is where the three
|
|
9573
|
+
* live, and it is pure.
|
|
9574
|
+
*
|
|
9537
9575
|
* Both are built on ONE per-graph record of the fingers
|
|
9538
9576
|
* on the glass, read from the NATIVE touch events -- see
|
|
9539
9577
|
* `touch_state_of` for why the pointer stream, which looks
|
|
@@ -9546,11 +9584,6 @@ function yui_graph_refresh_item(gobj, wide) {
|
|
|
9546
9584
|
* All Rights Reserved.
|
|
9547
9585
|
***********************************************************************/
|
|
9548
9586
|
/***************************************************************
|
|
9549
|
-
* Constants
|
|
9550
|
-
***************************************************************/
|
|
9551
|
-
var LONG_PRESS_MS = 500;
|
|
9552
|
-
var LONG_PRESS_SLOP = 10;
|
|
9553
|
-
/***************************************************************
|
|
9554
9587
|
* Per-graph record of the fingers on the glass
|
|
9555
9588
|
***************************************************************/
|
|
9556
9589
|
var __touch_state__ = /* @__PURE__ */ new WeakMap();
|
|
@@ -9577,7 +9610,9 @@ function touch_state_of(graph) {
|
|
|
9577
9610
|
if (state) return state;
|
|
9578
9611
|
state = {
|
|
9579
9612
|
count: 0,
|
|
9580
|
-
|
|
9613
|
+
multi: false,
|
|
9614
|
+
container: null,
|
|
9615
|
+
long_press_click: false
|
|
9581
9616
|
};
|
|
9582
9617
|
__touch_state__.set(graph, state);
|
|
9583
9618
|
let container = graph.getCanvas?.()?.getContainer?.();
|
|
@@ -9585,11 +9620,16 @@ function touch_state_of(graph) {
|
|
|
9585
9620
|
state.container = container;
|
|
9586
9621
|
let read = (event) => {
|
|
9587
9622
|
state.count = event.touches ? event.touches.length : 0;
|
|
9623
|
+
if (state.count >= 2) state.multi = true;
|
|
9624
|
+
else if (state.count === 0) state.multi = false;
|
|
9588
9625
|
};
|
|
9589
9626
|
container.addEventListener("touchstart", read, { passive: true });
|
|
9590
9627
|
container.addEventListener("touchmove", read, { passive: true });
|
|
9591
9628
|
container.addEventListener("touchend", read, { passive: true });
|
|
9592
9629
|
container.addEventListener("touchcancel", read, { passive: true });
|
|
9630
|
+
container.addEventListener("contextmenu", (event) => {
|
|
9631
|
+
if (state.count > 0) event.preventDefault();
|
|
9632
|
+
}, { capture: true });
|
|
9593
9633
|
return state;
|
|
9594
9634
|
}
|
|
9595
9635
|
/************************************************************
|
|
@@ -9690,6 +9730,24 @@ function ensure_pinch_zoom_patch() {
|
|
|
9690
9730
|
* Long press
|
|
9691
9731
|
***************************/
|
|
9692
9732
|
/************************************************************
|
|
9733
|
+
* Is the click being dispatched right now the tail of a long
|
|
9734
|
+
* press? Answers true ONCE, to the first asker.
|
|
9735
|
+
*
|
|
9736
|
+
* @antv/g does not take `click` from the DOM either: it makes
|
|
9737
|
+
* one out of the pointerdown/pointerup pair itself, inside
|
|
9738
|
+
* the same `onPointerUp` that just fed us the release. So
|
|
9739
|
+
* the press that opened the menu goes on to click whatever it
|
|
9740
|
+
* opened the menu ON -- selecting the node, or worse, dropping
|
|
9741
|
+
* a selection of several -- unless the handler asks.
|
|
9742
|
+
************************************************************/
|
|
9743
|
+
function consume_long_press_click(graph) {
|
|
9744
|
+
if (!graph) return false;
|
|
9745
|
+
let state = __touch_state__.get(graph);
|
|
9746
|
+
if (!state || !state.long_press_click) return false;
|
|
9747
|
+
state.long_press_click = false;
|
|
9748
|
+
return true;
|
|
9749
|
+
}
|
|
9750
|
+
/************************************************************
|
|
9693
9751
|
* Make a long press open the context menu.
|
|
9694
9752
|
*
|
|
9695
9753
|
* Returns the function that uninstalls it.
|
|
@@ -9699,19 +9757,34 @@ function ensure_pinch_zoom_patch() {
|
|
|
9699
9757
|
* the name the plugin listens for, so `getItems(e)` and
|
|
9700
9758
|
* `enable(e)` see the same object a right-click would give
|
|
9701
9759
|
* them -- same `target`, same `targetType`, same `client`.
|
|
9760
|
+
*
|
|
9761
|
+
* `options.enable(event)` decides, at the DOWN, whether this
|
|
9762
|
+
* press is even a candidate. Pass the plugin's own predicate:
|
|
9763
|
+
* a press on the empty canvas would otherwise arm a menu the
|
|
9764
|
+
* plugin then refuses to show, and swallow the click that
|
|
9765
|
+
* would have cleared the selection.
|
|
9702
9766
|
************************************************************/
|
|
9703
|
-
function install_long_press_contextmenu(graph) {
|
|
9767
|
+
function install_long_press_contextmenu(graph, options) {
|
|
9704
9768
|
if (!graph) return () => {};
|
|
9769
|
+
let enable = options?.enable;
|
|
9705
9770
|
let state = touch_state_of(graph);
|
|
9706
9771
|
let pending = null;
|
|
9707
|
-
let
|
|
9772
|
+
let disarm = () => {
|
|
9708
9773
|
if (pending) {
|
|
9709
|
-
clearTimeout(pending.
|
|
9774
|
+
clearTimeout(pending.tick);
|
|
9710
9775
|
pending = null;
|
|
9711
9776
|
}
|
|
9712
9777
|
};
|
|
9778
|
+
let announce = () => {
|
|
9779
|
+
if (!pending) return;
|
|
9780
|
+
if (state.multi) return;
|
|
9781
|
+
navigator.vibrate?.(15);
|
|
9782
|
+
};
|
|
9713
9783
|
let fire = (event) => {
|
|
9714
|
-
|
|
9784
|
+
state.long_press_click = true;
|
|
9785
|
+
setTimeout(() => {
|
|
9786
|
+
state.long_press_click = false;
|
|
9787
|
+
}, 0);
|
|
9715
9788
|
let swallow = (evt) => {
|
|
9716
9789
|
evt.stopPropagation();
|
|
9717
9790
|
document.removeEventListener("click", swallow, true);
|
|
@@ -9720,51 +9793,62 @@ function install_long_press_contextmenu(graph) {
|
|
|
9720
9793
|
setTimeout(() => {
|
|
9721
9794
|
document.removeEventListener("click", swallow, true);
|
|
9722
9795
|
}, 1e3);
|
|
9723
|
-
let container = graph.getCanvas?.()?.getContainer?.();
|
|
9724
|
-
if (container) container.addEventListener("contextmenu", (evt) => {
|
|
9725
|
-
evt.preventDefault();
|
|
9726
|
-
}, {
|
|
9727
|
-
once: true,
|
|
9728
|
-
capture: true
|
|
9729
|
-
});
|
|
9730
9796
|
graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
|
|
9731
9797
|
graph.emit(CommonEvent.CONTEXT_MENU, event);
|
|
9732
9798
|
};
|
|
9733
9799
|
let on_down = (event) => {
|
|
9734
|
-
|
|
9800
|
+
disarm();
|
|
9735
9801
|
if (event.pointerType !== "touch") return;
|
|
9736
|
-
if (state.count > 1) return;
|
|
9802
|
+
if (state.count > 1 || state.multi) return;
|
|
9803
|
+
if (enable && !enable(event)) return;
|
|
9737
9804
|
let x = event.client?.x;
|
|
9738
9805
|
let y = event.client?.y;
|
|
9739
9806
|
if (x === void 0 || y === void 0) return;
|
|
9740
|
-
let frozen = Object.assign({}, event);
|
|
9741
9807
|
pending = {
|
|
9742
9808
|
x,
|
|
9743
9809
|
y,
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9810
|
+
t0: Date.now(),
|
|
9811
|
+
event: Object.assign({}, event),
|
|
9812
|
+
tick: setTimeout(announce, 500)
|
|
9747
9813
|
};
|
|
9748
9814
|
};
|
|
9749
9815
|
let on_move = (event) => {
|
|
9750
9816
|
if (!pending) return;
|
|
9751
|
-
if (state.count > 1) {
|
|
9752
|
-
|
|
9817
|
+
if (state.count > 1 || state.multi) {
|
|
9818
|
+
disarm();
|
|
9753
9819
|
return;
|
|
9754
9820
|
}
|
|
9755
9821
|
let x = event.client?.x;
|
|
9756
9822
|
let y = event.client?.y;
|
|
9757
9823
|
if (x === void 0 || y === void 0) return;
|
|
9758
|
-
if (
|
|
9824
|
+
if (press_moved(pending, x, y)) disarm();
|
|
9759
9825
|
};
|
|
9826
|
+
let on_up = (event) => {
|
|
9827
|
+
let press = pending;
|
|
9828
|
+
if (press) clearTimeout(press.tick);
|
|
9829
|
+
pending = null;
|
|
9830
|
+
if (!press) return;
|
|
9831
|
+
if (state.multi) return;
|
|
9832
|
+
let x = event.client?.x;
|
|
9833
|
+
let y = event.client?.y;
|
|
9834
|
+
if (x === void 0) x = press.x;
|
|
9835
|
+
if (y === void 0) y = press.y;
|
|
9836
|
+
if (classify_press(press, x, y, Date.now()) !== "long") return;
|
|
9837
|
+
fire(press.event);
|
|
9838
|
+
};
|
|
9839
|
+
let on_touch_cancel = () => {
|
|
9840
|
+
disarm();
|
|
9841
|
+
};
|
|
9842
|
+
state.container?.addEventListener("touchcancel", on_touch_cancel);
|
|
9760
9843
|
graph.on(CommonEvent.POINTER_DOWN, on_down);
|
|
9761
9844
|
graph.on(CommonEvent.POINTER_MOVE, on_move);
|
|
9762
|
-
graph.on(CommonEvent.POINTER_UP,
|
|
9845
|
+
graph.on(CommonEvent.POINTER_UP, on_up);
|
|
9763
9846
|
return () => {
|
|
9764
|
-
|
|
9847
|
+
disarm();
|
|
9848
|
+
state.container?.removeEventListener("touchcancel", on_touch_cancel);
|
|
9765
9849
|
graph.off(CommonEvent.POINTER_DOWN, on_down);
|
|
9766
9850
|
graph.off(CommonEvent.POINTER_MOVE, on_move);
|
|
9767
|
-
graph.off(CommonEvent.POINTER_UP,
|
|
9851
|
+
graph.off(CommonEvent.POINTER_UP, on_up);
|
|
9768
9852
|
};
|
|
9769
9853
|
}
|
|
9770
9854
|
//#endregion
|
|
@@ -47964,6 +48048,7 @@ function configure_events(gobj) {
|
|
|
47964
48048
|
let priv = gobj.priv;
|
|
47965
48049
|
let graph = priv.graph;
|
|
47966
48050
|
graph.on(CanvasEvent.CLICK, (evt) => {
|
|
48051
|
+
if (consume_long_press_click(graph)) return;
|
|
47967
48052
|
gobj_send_event(gobj, "EV_CANVAS_CLICK", { evt }, gobj);
|
|
47968
48053
|
});
|
|
47969
48054
|
graph.on(NodeEvent.DRAG, (evt) => {
|
|
@@ -47977,12 +48062,14 @@ function configure_events(gobj) {
|
|
|
47977
48062
|
gobj_send_event(gobj, "EV_NODE_DRAG_END", { evt }, gobj);
|
|
47978
48063
|
});
|
|
47979
48064
|
graph.on(NodeEvent.CLICK, (evt) => {
|
|
48065
|
+
if (consume_long_press_click(graph)) return;
|
|
47980
48066
|
gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
|
|
47981
48067
|
});
|
|
47982
48068
|
graph.on(NodeEvent.CONTEXT_MENU, (evt) => {
|
|
47983
48069
|
gobj_send_event(gobj, "EV_NODE_CONTEXT_MENU", { evt }, gobj);
|
|
47984
48070
|
});
|
|
47985
48071
|
graph.on(EdgeEvent.CLICK, (evt) => {
|
|
48072
|
+
if (consume_long_press_click(graph)) return;
|
|
47986
48073
|
gobj_send_event(gobj, "EV_EDGE_CLICK", { evt }, gobj);
|
|
47987
48074
|
});
|
|
47988
48075
|
graph.on("aftertransform", () => {
|
|
@@ -48046,6 +48133,9 @@ function configure_plugins(gobj) {
|
|
|
48046
48133
|
update_toolbar(gobj);
|
|
48047
48134
|
}
|
|
48048
48135
|
});
|
|
48136
|
+
let menu_enable = (e) => {
|
|
48137
|
+
return e.targetType === "node" || e.targetType === "edge";
|
|
48138
|
+
};
|
|
48049
48139
|
graph_add_plugin(gobj, "contextmenu", {
|
|
48050
48140
|
trigger: "contextmenu",
|
|
48051
48141
|
onClick: (value) => {
|
|
@@ -48054,12 +48144,10 @@ function configure_plugins(gobj) {
|
|
|
48054
48144
|
getItems: (e) => {
|
|
48055
48145
|
return build_context_menu_items(gobj, e);
|
|
48056
48146
|
},
|
|
48057
|
-
enable:
|
|
48058
|
-
return e.targetType === "node" || e.targetType === "edge";
|
|
48059
|
-
}
|
|
48147
|
+
enable: menu_enable
|
|
48060
48148
|
});
|
|
48061
48149
|
if (priv._uninstall_long_press) priv._uninstall_long_press();
|
|
48062
|
-
priv._uninstall_long_press = install_long_press_contextmenu(priv.graph);
|
|
48150
|
+
priv._uninstall_long_press = install_long_press_contextmenu(priv.graph, { enable: menu_enable });
|
|
48063
48151
|
if (gobj_read_bool_attr(gobj, "with_toolbar")) {
|
|
48064
48152
|
configure_toolbar(gobj);
|
|
48065
48153
|
configure_toolbar_edit(gobj);
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
import {
|
|
112
112
|
ensure_pinch_zoom_patch,
|
|
113
113
|
install_long_press_contextmenu,
|
|
114
|
+
consume_long_press_click,
|
|
114
115
|
} from "./g6_touch_gestures.js";
|
|
115
116
|
import {yui_theme_now, yui_watch_theme} from "./yui_theme.js";
|
|
116
117
|
|
|
@@ -784,7 +785,14 @@ function configure_events(gobj)
|
|
|
784
785
|
let priv = gobj.priv;
|
|
785
786
|
let graph = priv.graph;
|
|
786
787
|
|
|
788
|
+
/* @antv/g makes a `click` out of every pointerdown/pointerup
|
|
789
|
+
* pair, this one included -- so the press that just opened the
|
|
790
|
+
* context menu goes on to click what it opened the menu ON. One
|
|
791
|
+
* press, one meaning: the press was already spent. */
|
|
787
792
|
graph.on(CanvasEvent.CLICK, (evt) => {
|
|
793
|
+
if(consume_long_press_click(graph)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
788
796
|
gobj_send_event(gobj, "EV_CANVAS_CLICK", {evt: evt}, gobj);
|
|
789
797
|
});
|
|
790
798
|
|
|
@@ -801,6 +809,9 @@ function configure_events(gobj)
|
|
|
801
809
|
});
|
|
802
810
|
|
|
803
811
|
graph.on(NodeEvent.CLICK, (evt) => {
|
|
812
|
+
if(consume_long_press_click(graph)) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
804
815
|
gobj_send_event(gobj, "EV_NODE_CLICK", {evt: evt}, gobj);
|
|
805
816
|
});
|
|
806
817
|
|
|
@@ -809,6 +820,9 @@ function configure_events(gobj)
|
|
|
809
820
|
});
|
|
810
821
|
|
|
811
822
|
graph.on(EdgeEvent.CLICK, (evt) => {
|
|
823
|
+
if(consume_long_press_click(graph)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
812
826
|
gobj_send_event(gobj, "EV_EDGE_CLICK", {evt: evt}, gobj);
|
|
813
827
|
});
|
|
814
828
|
|
|
@@ -948,6 +962,13 @@ function configure_plugins(gobj)
|
|
|
948
962
|
);
|
|
949
963
|
}
|
|
950
964
|
|
|
965
|
+
/* What the menu is FOR: a node or an edge. Shared with the long
|
|
966
|
+
* press below, so the gesture arms itself on exactly what the
|
|
967
|
+
* menu would agree to open on. */
|
|
968
|
+
let menu_enable = (e) => {
|
|
969
|
+
return e.targetType === 'node' || e.targetType === 'edge';
|
|
970
|
+
};
|
|
971
|
+
|
|
951
972
|
graph_add_plugin(
|
|
952
973
|
gobj,
|
|
953
974
|
'contextmenu',
|
|
@@ -959,9 +980,7 @@ function configure_plugins(gobj)
|
|
|
959
980
|
getItems: (e) => {
|
|
960
981
|
return build_context_menu_items(gobj, e);
|
|
961
982
|
},
|
|
962
|
-
enable:
|
|
963
|
-
return e.targetType === 'node' || e.targetType === 'edge';
|
|
964
|
-
},
|
|
983
|
+
enable: menu_enable,
|
|
965
984
|
}
|
|
966
985
|
);
|
|
967
986
|
|
|
@@ -976,7 +995,9 @@ function configure_plugins(gobj)
|
|
|
976
995
|
if(priv._uninstall_long_press) {
|
|
977
996
|
priv._uninstall_long_press();
|
|
978
997
|
}
|
|
979
|
-
priv._uninstall_long_press = install_long_press_contextmenu(
|
|
998
|
+
priv._uninstall_long_press = install_long_press_contextmenu(
|
|
999
|
+
priv.graph, {enable: menu_enable}
|
|
1000
|
+
);
|
|
980
1001
|
|
|
981
1002
|
if(gobj_read_bool_attr(gobj, "with_toolbar")) {
|
|
982
1003
|
configure_toolbar(gobj);
|
package/src/g6_touch_gestures.js
CHANGED
|
@@ -32,6 +32,21 @@
|
|
|
32
32
|
* forwarded event object -- which is why the plugin's
|
|
33
33
|
* `getItems(e)` receives exactly the shape it expects.
|
|
34
34
|
*
|
|
35
|
+
* ONE finger has to serve three commands -- move the
|
|
36
|
+
* node, act on it, open its menu -- so the press is
|
|
37
|
+
* arbitrated, and arbitrated at the RELEASE:
|
|
38
|
+
*
|
|
39
|
+
* moved -> drag
|
|
40
|
+
* still, let go fast -> the element's own action
|
|
41
|
+
* still, held >= 500ms -> the context menu
|
|
42
|
+
*
|
|
43
|
+
* A menu opened on a TIMER instead cannot be arbitrated
|
|
44
|
+
* at all: it opens while `drag-element` is already
|
|
45
|
+
* carrying the node, so the same press means both things
|
|
46
|
+
* and the user gets a menu over a card running away
|
|
47
|
+
* underneath it. `classify_press` is where the three
|
|
48
|
+
* live, and it is pure.
|
|
49
|
+
*
|
|
35
50
|
* Both are built on ONE per-graph record of the fingers
|
|
36
51
|
* on the glass, read from the NATIVE touch events -- see
|
|
37
52
|
* `touch_state_of` for why the pointer stream, which looks
|
|
@@ -50,19 +65,7 @@ import {
|
|
|
50
65
|
register,
|
|
51
66
|
} from '@antv/g6';
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
/***************************************************************
|
|
55
|
-
* Constants
|
|
56
|
-
***************************************************************/
|
|
57
|
-
|
|
58
|
-
/*
|
|
59
|
-
* How long a finger must stay put to mean "context menu", and how
|
|
60
|
-
* far it may wander first. 500ms is what Android and iOS both use
|
|
61
|
-
* for their own long press; a shorter one fires while the user is
|
|
62
|
-
* still deciding whether to pan.
|
|
63
|
-
*/
|
|
64
|
-
const LONG_PRESS_MS = 500;
|
|
65
|
-
const LONG_PRESS_SLOP = 10; /* CSS px */
|
|
68
|
+
import {press_moved, classify_press, LONG_PRESS_MS} from "./press_arbiter.js";
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
/***************************************************************
|
|
@@ -97,7 +100,11 @@ function touch_state_of(graph)
|
|
|
97
100
|
|
|
98
101
|
state = {
|
|
99
102
|
count: 0, /* fingers on the glass right now */
|
|
103
|
+
multi: false, /* two or more were down in THIS gesture */
|
|
100
104
|
container: null,
|
|
105
|
+
/* A long press fired, and the click that @antv/g makes out
|
|
106
|
+
* of the same pointerup has not been answered yet. */
|
|
107
|
+
long_press_click: false,
|
|
101
108
|
};
|
|
102
109
|
__touch_state__.set(graph, state);
|
|
103
110
|
|
|
@@ -109,6 +116,14 @@ function touch_state_of(graph)
|
|
|
109
116
|
|
|
110
117
|
let read = (event) => {
|
|
111
118
|
state.count = event.touches? event.touches.length: 0;
|
|
119
|
+
if(state.count >= 2) {
|
|
120
|
+
/* Sticky until the LAST finger goes: a pinch releases its
|
|
121
|
+
* two fingers one at a time, and the release of the first
|
|
122
|
+
* one must not be read as the end of a press. */
|
|
123
|
+
state.multi = true;
|
|
124
|
+
} else if(state.count === 0) {
|
|
125
|
+
state.multi = false;
|
|
126
|
+
}
|
|
112
127
|
};
|
|
113
128
|
|
|
114
129
|
/* Passive: we never preventDefault here -- the canvas already
|
|
@@ -119,6 +134,24 @@ function touch_state_of(graph)
|
|
|
119
134
|
container.addEventListener("touchend", read, {passive: true});
|
|
120
135
|
container.addEventListener("touchcancel", read, {passive: true});
|
|
121
136
|
|
|
137
|
+
/*
|
|
138
|
+
* The browser opens its OWN menu on a long press, and it opens it
|
|
139
|
+
* while the finger is still down -- on top of whatever the graph
|
|
140
|
+
* is about to do with the same press. Refuse it for as long as
|
|
141
|
+
* there is a finger on the glass.
|
|
142
|
+
*
|
|
143
|
+
* This costs the graph nothing: G6 reads no `contextmenu` from
|
|
144
|
+
* the DOM at all (it synthesises its own from `pointerdown` with
|
|
145
|
+
* `button === 2`), so a real right click keeps working with its
|
|
146
|
+
* DOM event refused -- and it is not refused here anyway, because
|
|
147
|
+
* a mouse leaves `count` at zero.
|
|
148
|
+
*/
|
|
149
|
+
container.addEventListener("contextmenu", (event) => {
|
|
150
|
+
if(state.count > 0) {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
}
|
|
153
|
+
}, {capture: true});
|
|
154
|
+
|
|
122
155
|
return state;
|
|
123
156
|
}
|
|
124
157
|
|
|
@@ -290,6 +323,30 @@ export function ensure_pinch_zoom_patch()
|
|
|
290
323
|
|
|
291
324
|
|
|
292
325
|
|
|
326
|
+
/************************************************************
|
|
327
|
+
* Is the click being dispatched right now the tail of a long
|
|
328
|
+
* press? Answers true ONCE, to the first asker.
|
|
329
|
+
*
|
|
330
|
+
* @antv/g does not take `click` from the DOM either: it makes
|
|
331
|
+
* one out of the pointerdown/pointerup pair itself, inside
|
|
332
|
+
* the same `onPointerUp` that just fed us the release. So
|
|
333
|
+
* the press that opened the menu goes on to click whatever it
|
|
334
|
+
* opened the menu ON -- selecting the node, or worse, dropping
|
|
335
|
+
* a selection of several -- unless the handler asks.
|
|
336
|
+
************************************************************/
|
|
337
|
+
export function consume_long_press_click(graph)
|
|
338
|
+
{
|
|
339
|
+
if(!graph) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
let state = __touch_state__.get(graph);
|
|
343
|
+
if(!state || !state.long_press_click) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
state.long_press_click = false;
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
293
350
|
/************************************************************
|
|
294
351
|
* Make a long press open the context menu.
|
|
295
352
|
*
|
|
@@ -300,33 +357,71 @@ export function ensure_pinch_zoom_patch()
|
|
|
300
357
|
* the name the plugin listens for, so `getItems(e)` and
|
|
301
358
|
* `enable(e)` see the same object a right-click would give
|
|
302
359
|
* them -- same `target`, same `targetType`, same `client`.
|
|
360
|
+
*
|
|
361
|
+
* `options.enable(event)` decides, at the DOWN, whether this
|
|
362
|
+
* press is even a candidate. Pass the plugin's own predicate:
|
|
363
|
+
* a press on the empty canvas would otherwise arm a menu the
|
|
364
|
+
* plugin then refuses to show, and swallow the click that
|
|
365
|
+
* would have cleared the selection.
|
|
303
366
|
************************************************************/
|
|
304
|
-
export function install_long_press_contextmenu(graph)
|
|
367
|
+
export function install_long_press_contextmenu(graph, options)
|
|
305
368
|
{
|
|
306
369
|
if(!graph) {
|
|
307
370
|
return () => {};
|
|
308
371
|
}
|
|
309
372
|
|
|
310
|
-
let
|
|
311
|
-
let
|
|
373
|
+
let enable = options?.enable;
|
|
374
|
+
let state = touch_state_of(graph);
|
|
375
|
+
let pending = null; /* {x, y, t0, event, tick} */
|
|
312
376
|
|
|
313
|
-
let
|
|
377
|
+
let disarm = () => {
|
|
314
378
|
if(pending) {
|
|
315
|
-
clearTimeout(pending.
|
|
379
|
+
clearTimeout(pending.tick);
|
|
316
380
|
pending = null;
|
|
317
381
|
}
|
|
318
382
|
};
|
|
319
383
|
|
|
384
|
+
/*
|
|
385
|
+
* The one thing deciding at the RELEASE costs: while the finger
|
|
386
|
+
* is down, nothing on the screen says the menu is now what
|
|
387
|
+
* letting go would give you. A tick of haptics says it.
|
|
388
|
+
*
|
|
389
|
+
* It is a NOTICE, not the decision -- the arbitration is still
|
|
390
|
+
* entirely in `on_up`. So a finger that buzzes and then carries
|
|
391
|
+
* the node away gets its drag, exactly as if it had never
|
|
392
|
+
* buzzed; the tick only reports what the press is worth at the
|
|
393
|
+
* moment it crosses the line. (Where there is no vibrator --
|
|
394
|
+
* iOS Safari, a desktop -- `vibrate` is simply absent and this
|
|
395
|
+
* is a no-op.)
|
|
396
|
+
*/
|
|
397
|
+
let announce = () => {
|
|
398
|
+
if(!pending) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if(state.multi) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
navigator.vibrate?.(15);
|
|
405
|
+
};
|
|
406
|
+
|
|
320
407
|
let fire = (event) => {
|
|
321
|
-
|
|
408
|
+
/*
|
|
409
|
+
* The click @antv/g is about to make out of this same
|
|
410
|
+
* pointerup belongs to the press, and the press has already
|
|
411
|
+
* been spent on the menu.
|
|
412
|
+
*/
|
|
413
|
+
state.long_press_click = true;
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
state.long_press_click = false;
|
|
416
|
+
}, 0);
|
|
322
417
|
|
|
323
418
|
/*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
419
|
+
* That click also reaches the plugin's own `document`
|
|
420
|
+
* listener, which hides the menu the press just opened -- a
|
|
421
|
+
* right click never produces a `click`, so the plugin has no
|
|
422
|
+
* reason to guard against it. Swallow that one click in the
|
|
423
|
+
* CAPTURE phase, where document sees it before anything else
|
|
424
|
+
* does.
|
|
330
425
|
*/
|
|
331
426
|
let swallow = (evt) => {
|
|
332
427
|
evt.stopPropagation();
|
|
@@ -337,30 +432,21 @@ export function install_long_press_contextmenu(graph)
|
|
|
337
432
|
document.removeEventListener("click", swallow, true);
|
|
338
433
|
}, 1000);
|
|
339
434
|
|
|
340
|
-
/*
|
|
341
|
-
* And the browser's own long-press menu, which would open on
|
|
342
|
-
* top of ours. One shot: a later right click must keep
|
|
343
|
-
* reaching the plugin.
|
|
344
|
-
*/
|
|
345
|
-
let container = graph.getCanvas?.()?.getContainer?.();
|
|
346
|
-
if(container) {
|
|
347
|
-
container.addEventListener("contextmenu", (evt) => {
|
|
348
|
-
evt.preventDefault();
|
|
349
|
-
}, {once: true, capture: true});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
435
|
graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
|
|
353
436
|
graph.emit(CommonEvent.CONTEXT_MENU, event);
|
|
354
437
|
};
|
|
355
438
|
|
|
356
439
|
let on_down = (event) => {
|
|
357
|
-
|
|
440
|
+
disarm();
|
|
358
441
|
if(event.pointerType !== "touch") {
|
|
359
442
|
return; /* a mouse holding its button is not a menu */
|
|
360
443
|
}
|
|
361
|
-
if(state.count > 1) {
|
|
444
|
+
if(state.count > 1 || state.multi) {
|
|
362
445
|
return; /* two fingers: that is the pinch */
|
|
363
446
|
}
|
|
447
|
+
if(enable && !enable(event)) {
|
|
448
|
+
return; /* nothing here has a menu to open */
|
|
449
|
+
}
|
|
364
450
|
let x = event.client?.x;
|
|
365
451
|
let y = event.client?.y;
|
|
366
452
|
if(x === undefined || y === undefined) {
|
|
@@ -368,16 +454,16 @@ export function install_long_press_contextmenu(graph)
|
|
|
368
454
|
}
|
|
369
455
|
/*
|
|
370
456
|
* The event object is reused by @antv/g between dispatches,
|
|
371
|
-
* so what the
|
|
372
|
-
*
|
|
457
|
+
* so what the release must answer with is a COPY -- by then
|
|
458
|
+
* the live object describes the pointerup, and the menu is
|
|
459
|
+
* about what was PRESSED.
|
|
373
460
|
*/
|
|
374
|
-
let frozen = Object.assign({}, event);
|
|
375
461
|
pending = {
|
|
376
|
-
x:
|
|
377
|
-
y:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
462
|
+
x: x,
|
|
463
|
+
y: y,
|
|
464
|
+
t0: Date.now(),
|
|
465
|
+
event: Object.assign({}, event),
|
|
466
|
+
tick: setTimeout(announce, LONG_PRESS_MS),
|
|
381
467
|
};
|
|
382
468
|
};
|
|
383
469
|
|
|
@@ -385,8 +471,8 @@ export function install_long_press_contextmenu(graph)
|
|
|
385
471
|
if(!pending) {
|
|
386
472
|
return;
|
|
387
473
|
}
|
|
388
|
-
if(state.count > 1) {
|
|
389
|
-
|
|
474
|
+
if(state.count > 1 || state.multi) {
|
|
475
|
+
disarm();
|
|
390
476
|
return;
|
|
391
477
|
}
|
|
392
478
|
let x = event.client?.x;
|
|
@@ -394,20 +480,54 @@ export function install_long_press_contextmenu(graph)
|
|
|
394
480
|
if(x === undefined || y === undefined) {
|
|
395
481
|
return;
|
|
396
482
|
}
|
|
397
|
-
if(
|
|
398
|
-
|
|
399
|
-
cancel();
|
|
483
|
+
if(press_moved(pending, x, y)) {
|
|
484
|
+
disarm(); /* the finger is carrying the node */
|
|
400
485
|
}
|
|
401
486
|
};
|
|
402
487
|
|
|
488
|
+
let on_up = (event) => {
|
|
489
|
+
let press = pending;
|
|
490
|
+
if(press) {
|
|
491
|
+
clearTimeout(press.tick);
|
|
492
|
+
}
|
|
493
|
+
pending = null;
|
|
494
|
+
if(!press) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if(state.multi) {
|
|
498
|
+
return; /* a finger of a pinch, not a press */
|
|
499
|
+
}
|
|
500
|
+
let x = event.client?.x;
|
|
501
|
+
let y = event.client?.y;
|
|
502
|
+
if(x === undefined) {
|
|
503
|
+
x = press.x;
|
|
504
|
+
}
|
|
505
|
+
if(y === undefined) {
|
|
506
|
+
y = press.y;
|
|
507
|
+
}
|
|
508
|
+
if(classify_press(press, x, y, Date.now()) !== "long") {
|
|
509
|
+
return; /* a drag, or a tap: both are already served */
|
|
510
|
+
}
|
|
511
|
+
fire(press.event);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
/* The gesture can also end without a release: the browser takes
|
|
515
|
+
* the touch away (a system gesture, a call), and the press must
|
|
516
|
+
* not survive it into the next one. */
|
|
517
|
+
let on_touch_cancel = () => {
|
|
518
|
+
disarm();
|
|
519
|
+
};
|
|
520
|
+
state.container?.addEventListener("touchcancel", on_touch_cancel);
|
|
521
|
+
|
|
403
522
|
graph.on(CommonEvent.POINTER_DOWN, on_down);
|
|
404
523
|
graph.on(CommonEvent.POINTER_MOVE, on_move);
|
|
405
|
-
graph.on(CommonEvent.POINTER_UP,
|
|
524
|
+
graph.on(CommonEvent.POINTER_UP, on_up);
|
|
406
525
|
|
|
407
526
|
return () => {
|
|
408
|
-
|
|
527
|
+
disarm();
|
|
528
|
+
state.container?.removeEventListener("touchcancel", on_touch_cancel);
|
|
409
529
|
graph.off(CommonEvent.POINTER_DOWN, on_down);
|
|
410
530
|
graph.off(CommonEvent.POINTER_MOVE, on_move);
|
|
411
|
-
graph.off(CommonEvent.POINTER_UP,
|
|
531
|
+
graph.off(CommonEvent.POINTER_UP, on_up);
|
|
412
532
|
};
|
|
413
533
|
}
|
package/src/lib_graph.css
CHANGED
|
@@ -52,6 +52,27 @@
|
|
|
52
52
|
.graph-container {
|
|
53
53
|
-webkit-user-select: none;
|
|
54
54
|
user-select: none;
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* And the finger's half of the same problem: on a touch screen
|
|
58
|
+
* the browser owns the gesture until something says otherwise.
|
|
59
|
+
*
|
|
60
|
+
* G6 puts `touch-action: none` on its CANVAS, so dragging from
|
|
61
|
+
* the background pans the graph -- and it puts nothing on the
|
|
62
|
+
* HTML nodes, which are ordinary DIVs layered on top of it. So
|
|
63
|
+
* a finger that lands on a CARD was making a page scroll:
|
|
64
|
+
* Chrome let two `pointermove`s through, decided the gesture
|
|
65
|
+
* was a scroll, and killed the pointer stream with a
|
|
66
|
+
* `pointercancel`. The node followed the finger for about 20px
|
|
67
|
+
* and then stopped dead while the page slid -- which is what
|
|
68
|
+
* "I only want to move a node" looked like on a phone.
|
|
69
|
+
*
|
|
70
|
+
* The whole container, not the cards: every layer G6 stacks in
|
|
71
|
+
* here (nodes, plugin containers, the ones it adds next) is a
|
|
72
|
+
* DIV over a canvas that already refuses the browser's
|
|
73
|
+
* gestures, and they have to agree.
|
|
74
|
+
*/
|
|
75
|
+
touch-action: none;
|
|
55
76
|
}
|
|
56
77
|
.graph-container input,
|
|
57
78
|
.graph-container textarea,
|
|
@@ -66,6 +87,19 @@
|
|
|
66
87
|
user-select: text;
|
|
67
88
|
}
|
|
68
89
|
|
|
90
|
+
/*
|
|
91
|
+
* What a finger must still be able to SCROLL: the panels, and the
|
|
92
|
+
* context menu, which is as long as the impact of a delete.
|
|
93
|
+
*/
|
|
94
|
+
.graph-container .g6-confirm-popover,
|
|
95
|
+
.graph-container .g6-create-popover,
|
|
96
|
+
.graph-container .g6-node-popover,
|
|
97
|
+
.graph-container .g6-edge-popover,
|
|
98
|
+
.graph-container .g6-node-detail,
|
|
99
|
+
.graph-container .g6-contextmenu {
|
|
100
|
+
touch-action: auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
.graph-container:fullscreen {
|
|
70
104
|
background-color: var(--bulma-background, #fff);
|
|
71
105
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* press_arbiter.js
|
|
3
|
+
*
|
|
4
|
+
* What ONE finger meant.
|
|
5
|
+
*
|
|
6
|
+
* A graph gives a single finger three commands -- move the
|
|
7
|
+
* node, act on it, open its menu -- and nothing in the press
|
|
8
|
+
* says which one until the finger either moves or lets go.
|
|
9
|
+
* So the press is arbitrated, and arbitrated at the RELEASE:
|
|
10
|
+
*
|
|
11
|
+
* moved -> "drag" (the node followed it)
|
|
12
|
+
* still, let go fast -> "tap" (the element's action)
|
|
13
|
+
* still, held >= 500ms -> "long" (the context menu)
|
|
14
|
+
*
|
|
15
|
+
* Deciding earlier is what breaks it. A menu opened on a
|
|
16
|
+
* TIMER opens while `drag-element` is already carrying the
|
|
17
|
+
* node: the same press then means two things at once, and
|
|
18
|
+
* the user gets a menu over a card running away underneath
|
|
19
|
+
* it. A timer cannot arbitrate, because at the moment it
|
|
20
|
+
* fires the gesture is not over.
|
|
21
|
+
*
|
|
22
|
+
* Pure, and separate from `g6_touch_gestures.js` for that
|
|
23
|
+
* reason: the rule can be read and tested without a graph,
|
|
24
|
+
* a canvas or a finger.
|
|
25
|
+
*
|
|
26
|
+
* Copyright (c) 2026, ArtGins.
|
|
27
|
+
* All Rights Reserved.
|
|
28
|
+
***********************************************************************/
|
|
29
|
+
|
|
30
|
+
/***************************************************************
|
|
31
|
+
* Constants
|
|
32
|
+
***************************************************************/
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* How long a finger must stay put to mean "context menu", and how
|
|
36
|
+
* far it may wander first. 500ms is what Android and iOS both use
|
|
37
|
+
* for their own long press.
|
|
38
|
+
*
|
|
39
|
+
* The 10px is not a taste: it is the `dragstartDistanceThreshold`
|
|
40
|
+
* G6 gives the dragndrop plugin of its main canvas. Keeping the
|
|
41
|
+
* two equal is what makes "still" mean the same thing to both --
|
|
42
|
+
* a press held inside the slop has NOT moved the node, so a menu
|
|
43
|
+
* it opens does not open over a card that quietly shifted.
|
|
44
|
+
*/
|
|
45
|
+
export const LONG_PRESS_MS = 500;
|
|
46
|
+
export const LONG_PRESS_SLOP = 10; /* CSS px */
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/***************************************************************
|
|
50
|
+
* Arbitration
|
|
51
|
+
***************************************************************/
|
|
52
|
+
|
|
53
|
+
/************************************************************
|
|
54
|
+
* Did the finger travel far enough to mean a DRAG?
|
|
55
|
+
*
|
|
56
|
+
* `press` is what the pointerdown recorded: {x, y, t0}.
|
|
57
|
+
************************************************************/
|
|
58
|
+
export function press_moved(press, x, y)
|
|
59
|
+
{
|
|
60
|
+
if(!press) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return Math.abs(x - press.x) > LONG_PRESS_SLOP ||
|
|
64
|
+
Math.abs(y - press.y) > LONG_PRESS_SLOP;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/************************************************************
|
|
68
|
+
* What the press meant, asked when the finger comes up.
|
|
69
|
+
*
|
|
70
|
+
* Answers "none" when there was no press to arbitrate.
|
|
71
|
+
************************************************************/
|
|
72
|
+
export function classify_press(press, x, y, t)
|
|
73
|
+
{
|
|
74
|
+
if(!press) {
|
|
75
|
+
return "none";
|
|
76
|
+
}
|
|
77
|
+
if(press_moved(press, x, y)) {
|
|
78
|
+
return "drag";
|
|
79
|
+
}
|
|
80
|
+
if(t - press.t0 >= LONG_PRESS_MS) {
|
|
81
|
+
return "long";
|
|
82
|
+
}
|
|
83
|
+
return "tap";
|
|
84
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* press_arbiter.test.js
|
|
3
|
+
***********************************************************************/
|
|
4
|
+
import {describe, it, expect} from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
LONG_PRESS_MS,
|
|
7
|
+
LONG_PRESS_SLOP,
|
|
8
|
+
press_moved,
|
|
9
|
+
classify_press,
|
|
10
|
+
} from "./press_arbiter.js";
|
|
11
|
+
|
|
12
|
+
const at = (x, y, t0) => ({x: x, y: y, t0: t0});
|
|
13
|
+
|
|
14
|
+
describe("press_moved", () => {
|
|
15
|
+
it("a finger that stays inside the slop has not moved", () => {
|
|
16
|
+
let p = at(100, 100, 0);
|
|
17
|
+
expect(press_moved(p, 100, 100)).toBe(false);
|
|
18
|
+
expect(press_moved(p, 100 + LONG_PRESS_SLOP, 100)).toBe(false);
|
|
19
|
+
expect(press_moved(p, 100, 100 - LONG_PRESS_SLOP)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("one pixel past the slop, in either axis, is a move", () => {
|
|
23
|
+
let p = at(100, 100, 0);
|
|
24
|
+
expect(press_moved(p, 100 + LONG_PRESS_SLOP + 1, 100)).toBe(true);
|
|
25
|
+
expect(press_moved(p, 100, 100 + LONG_PRESS_SLOP + 1)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("no press, no move", () => {
|
|
29
|
+
expect(press_moved(null, 100, 100)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("classify_press", () => {
|
|
34
|
+
it("a press that moved is a drag, however long it was held", () => {
|
|
35
|
+
let p = at(100, 100, 0);
|
|
36
|
+
expect(classify_press(p, 400, 100, 10)).toBe("drag");
|
|
37
|
+
expect(classify_press(p, 400, 100, 10 * LONG_PRESS_MS)).toBe("drag");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("still and let go quickly is a tap", () => {
|
|
41
|
+
let p = at(100, 100, 1000);
|
|
42
|
+
expect(classify_press(p, 100, 100, 1000)).toBe("tap");
|
|
43
|
+
expect(classify_press(p, 105, 103, 1000 + LONG_PRESS_MS - 1)).toBe("tap");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("still and held to the threshold is the menu", () => {
|
|
47
|
+
let p = at(100, 100, 1000);
|
|
48
|
+
expect(classify_press(p, 100, 100, 1000 + LONG_PRESS_MS)).toBe("long");
|
|
49
|
+
expect(classify_press(p, 108, 100, 9000)).toBe("long");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("a long hold that ends somewhere else is still a drag", () => {
|
|
53
|
+
/* The finger that thought about the menu and then carried the
|
|
54
|
+
* node is carrying the node: the release is what decides, and
|
|
55
|
+
* the release is far away. */
|
|
56
|
+
let p = at(100, 100, 0);
|
|
57
|
+
expect(classify_press(p, 260, 340, 3000)).toBe("drag");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("no press to arbitrate", () => {
|
|
61
|
+
expect(classify_press(null, 0, 0, 0)).toBe("none");
|
|
62
|
+
});
|
|
63
|
+
});
|