@yuneta/gobj-ui 7.23.13 → 7.23.14

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 CHANGED
@@ -854,6 +854,26 @@ 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. Firing the menu on a TIMER instead cannot arbitrate anything,
861
+ because at the moment it fires the gesture is not over: it opened while
862
+ `drag-element` was already carrying the node, so one press meant both things
863
+ and the menu sat over a card running away underneath it. Two more things the
864
+ same press was doing, both fixed with it: `click` is not taken from the DOM
865
+ either (`@antv/g`'s `onPointerUp` synthesises one), so the press that opened
866
+ the menu went on to click the node it opened the menu on — the click
867
+ handlers ask `consume_long_press_click()` first; and the BROWSER's own menu,
868
+ which opens while the finger is still down, is refused for as long as there
869
+ is a finger on the glass.
870
+ - **The browser was taking the gesture outright.** G6 puts `touch-action:
871
+ none` on its canvas and nothing on its HTML nodes, which are ordinary DIVs
872
+ over it — so a drag that started on a **card** was a page scroll: two
873
+ `pointermove`s through, then `pointercancel`, and the node stopped dead
874
+ about 20px in while the page slid. `.graph-container` refuses those gestures
875
+ whole now; the panels and the context menu keep `touch-action: auto`, so a
876
+ finger still scrolls what is meant to scroll.
857
877
 
858
878
  Everything a finger has to LAND on is sized off `(pointer: coarse)`, which
859
879
  means **a mouse sees no change at all**: node resize handles become a 14px
@@ -884,9 +904,9 @@ gesture to give them: G6 binds panning and the band to the same plain drag, so
884
904
  one of the two has to stand aside. The edit toolbar carries a **selection
885
905
  mode** toggle (the dashed marquee, next to `+`): while it is on, a tap picks a
886
906
  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 inverted
888
- neutral chip) rather than taking one of the palette's colours: each of those
889
- names a KIND of action, and `7.23.11` borrowed undo/redo's violet for it —
907
+ aside. It looks **pressed** while it is on (`7.23.12`, `pressed_state`, an
908
+ inverted neutral chip) rather than taking one of the palette's colours: each of
909
+ those names a KIND of action, and `7.23.11` borrowed undo/redo's violet for it —
890
910
  which put the same colour on two neighbouring buttons for two different
891
911
  reasons, and carried the whole state change in the hairline of an outline
892
912
  glyph. It is deliberately a button and not a heuristic — the toolbar says which
@@ -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
- container: null
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,26 @@ 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 cancel = () => {
9713
- if (pending) {
9714
- clearTimeout(pending.timer);
9715
- pending = null;
9716
- }
9777
+ let disarm = () => {
9778
+ pending = null;
9717
9779
  };
9718
9780
  let fire = (event) => {
9719
- pending = null;
9781
+ state.long_press_click = true;
9782
+ setTimeout(() => {
9783
+ state.long_press_click = false;
9784
+ }, 0);
9720
9785
  let swallow = (evt) => {
9721
9786
  evt.stopPropagation();
9722
9787
  document.removeEventListener("click", swallow, true);
@@ -9725,51 +9790,60 @@ function install_long_press_contextmenu(graph) {
9725
9790
  setTimeout(() => {
9726
9791
  document.removeEventListener("click", swallow, true);
9727
9792
  }, 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
9793
  graph.emit(`${event.targetType}:${_antv_g6.CommonEvent.CONTEXT_MENU}`, event);
9736
9794
  graph.emit(_antv_g6.CommonEvent.CONTEXT_MENU, event);
9737
9795
  };
9738
9796
  let on_down = (event) => {
9739
- cancel();
9797
+ disarm();
9740
9798
  if (event.pointerType !== "touch") return;
9741
- if (state.count > 1) return;
9799
+ if (state.count > 1 || state.multi) return;
9800
+ if (enable && !enable(event)) return;
9742
9801
  let x = event.client?.x;
9743
9802
  let y = event.client?.y;
9744
9803
  if (x === void 0 || y === void 0) return;
9745
- let frozen = Object.assign({}, event);
9746
9804
  pending = {
9747
9805
  x,
9748
9806
  y,
9749
- timer: setTimeout(() => {
9750
- fire(frozen);
9751
- }, LONG_PRESS_MS)
9807
+ t0: Date.now(),
9808
+ event: Object.assign({}, event)
9752
9809
  };
9753
9810
  };
9754
9811
  let on_move = (event) => {
9755
9812
  if (!pending) return;
9756
- if (state.count > 1) {
9757
- cancel();
9813
+ if (state.count > 1 || state.multi) {
9814
+ disarm();
9758
9815
  return;
9759
9816
  }
9760
9817
  let x = event.client?.x;
9761
9818
  let y = event.client?.y;
9762
9819
  if (x === void 0 || y === void 0) return;
9763
- if (Math.abs(x - pending.x) > LONG_PRESS_SLOP || Math.abs(y - pending.y) > LONG_PRESS_SLOP) cancel();
9820
+ if (press_moved(pending, x, y)) disarm();
9764
9821
  };
9822
+ let on_up = (event) => {
9823
+ let press = pending;
9824
+ pending = null;
9825
+ if (!press) return;
9826
+ if (state.multi) return;
9827
+ let x = event.client?.x;
9828
+ let y = event.client?.y;
9829
+ if (x === void 0) x = press.x;
9830
+ if (y === void 0) y = press.y;
9831
+ if (classify_press(press, x, y, Date.now()) !== "long") return;
9832
+ fire(press.event);
9833
+ };
9834
+ let on_touch_cancel = () => {
9835
+ disarm();
9836
+ };
9837
+ state.container?.addEventListener("touchcancel", on_touch_cancel);
9765
9838
  graph.on(_antv_g6.CommonEvent.POINTER_DOWN, on_down);
9766
9839
  graph.on(_antv_g6.CommonEvent.POINTER_MOVE, on_move);
9767
- graph.on(_antv_g6.CommonEvent.POINTER_UP, cancel);
9840
+ graph.on(_antv_g6.CommonEvent.POINTER_UP, on_up);
9768
9841
  return () => {
9769
- cancel();
9842
+ disarm();
9843
+ state.container?.removeEventListener("touchcancel", on_touch_cancel);
9770
9844
  graph.off(_antv_g6.CommonEvent.POINTER_DOWN, on_down);
9771
9845
  graph.off(_antv_g6.CommonEvent.POINTER_MOVE, on_move);
9772
- graph.off(_antv_g6.CommonEvent.POINTER_UP, cancel);
9846
+ graph.off(_antv_g6.CommonEvent.POINTER_UP, on_up);
9773
9847
  };
9774
9848
  }
9775
9849
  //#endregion
@@ -47977,6 +48051,7 @@ function configure_events(gobj) {
47977
48051
  let priv = gobj.priv;
47978
48052
  let graph = priv.graph;
47979
48053
  graph.on(_antv_g6.CanvasEvent.CLICK, (evt) => {
48054
+ if (consume_long_press_click(graph)) return;
47980
48055
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CANVAS_CLICK", { evt }, gobj);
47981
48056
  });
47982
48057
  graph.on(_antv_g6.NodeEvent.DRAG, (evt) => {
@@ -47990,12 +48065,14 @@ function configure_events(gobj) {
47990
48065
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_DRAG_END", { evt }, gobj);
47991
48066
  });
47992
48067
  graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
48068
+ if (consume_long_press_click(graph)) return;
47993
48069
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
47994
48070
  });
47995
48071
  graph.on(_antv_g6.NodeEvent.CONTEXT_MENU, (evt) => {
47996
48072
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CONTEXT_MENU", { evt }, gobj);
47997
48073
  });
47998
48074
  graph.on(_antv_g6.EdgeEvent.CLICK, (evt) => {
48075
+ if (consume_long_press_click(graph)) return;
47999
48076
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_EDGE_CLICK", { evt }, gobj);
48000
48077
  });
48001
48078
  graph.on("aftertransform", () => {
@@ -48059,6 +48136,9 @@ function configure_plugins(gobj) {
48059
48136
  update_toolbar(gobj);
48060
48137
  }
48061
48138
  });
48139
+ let menu_enable = (e) => {
48140
+ return e.targetType === "node" || e.targetType === "edge";
48141
+ };
48062
48142
  graph_add_plugin(gobj, "contextmenu", {
48063
48143
  trigger: "contextmenu",
48064
48144
  onClick: (value) => {
@@ -48067,12 +48147,10 @@ function configure_plugins(gobj) {
48067
48147
  getItems: (e) => {
48068
48148
  return build_context_menu_items(gobj, e);
48069
48149
  },
48070
- enable: (e) => {
48071
- return e.targetType === "node" || e.targetType === "edge";
48072
- }
48150
+ enable: menu_enable
48073
48151
  });
48074
48152
  if (priv._uninstall_long_press) priv._uninstall_long_press();
48075
- priv._uninstall_long_press = install_long_press_contextmenu(priv.graph);
48153
+ priv._uninstall_long_press = install_long_press_contextmenu(priv.graph, { enable: menu_enable });
48076
48154
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_toolbar")) {
48077
48155
  configure_toolbar(gobj);
48078
48156
  configure_toolbar_edit(gobj);
@@ -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
- container: null
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,26 @@ 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 cancel = () => {
9708
- if (pending) {
9709
- clearTimeout(pending.timer);
9710
- pending = null;
9711
- }
9772
+ let disarm = () => {
9773
+ pending = null;
9712
9774
  };
9713
9775
  let fire = (event) => {
9714
- pending = null;
9776
+ state.long_press_click = true;
9777
+ setTimeout(() => {
9778
+ state.long_press_click = false;
9779
+ }, 0);
9715
9780
  let swallow = (evt) => {
9716
9781
  evt.stopPropagation();
9717
9782
  document.removeEventListener("click", swallow, true);
@@ -9720,51 +9785,60 @@ function install_long_press_contextmenu(graph) {
9720
9785
  setTimeout(() => {
9721
9786
  document.removeEventListener("click", swallow, true);
9722
9787
  }, 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
9788
  graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
9731
9789
  graph.emit(CommonEvent.CONTEXT_MENU, event);
9732
9790
  };
9733
9791
  let on_down = (event) => {
9734
- cancel();
9792
+ disarm();
9735
9793
  if (event.pointerType !== "touch") return;
9736
- if (state.count > 1) return;
9794
+ if (state.count > 1 || state.multi) return;
9795
+ if (enable && !enable(event)) return;
9737
9796
  let x = event.client?.x;
9738
9797
  let y = event.client?.y;
9739
9798
  if (x === void 0 || y === void 0) return;
9740
- let frozen = Object.assign({}, event);
9741
9799
  pending = {
9742
9800
  x,
9743
9801
  y,
9744
- timer: setTimeout(() => {
9745
- fire(frozen);
9746
- }, LONG_PRESS_MS)
9802
+ t0: Date.now(),
9803
+ event: Object.assign({}, event)
9747
9804
  };
9748
9805
  };
9749
9806
  let on_move = (event) => {
9750
9807
  if (!pending) return;
9751
- if (state.count > 1) {
9752
- cancel();
9808
+ if (state.count > 1 || state.multi) {
9809
+ disarm();
9753
9810
  return;
9754
9811
  }
9755
9812
  let x = event.client?.x;
9756
9813
  let y = event.client?.y;
9757
9814
  if (x === void 0 || y === void 0) return;
9758
- if (Math.abs(x - pending.x) > LONG_PRESS_SLOP || Math.abs(y - pending.y) > LONG_PRESS_SLOP) cancel();
9815
+ if (press_moved(pending, x, y)) disarm();
9759
9816
  };
9817
+ let on_up = (event) => {
9818
+ let press = pending;
9819
+ pending = null;
9820
+ if (!press) return;
9821
+ if (state.multi) return;
9822
+ let x = event.client?.x;
9823
+ let y = event.client?.y;
9824
+ if (x === void 0) x = press.x;
9825
+ if (y === void 0) y = press.y;
9826
+ if (classify_press(press, x, y, Date.now()) !== "long") return;
9827
+ fire(press.event);
9828
+ };
9829
+ let on_touch_cancel = () => {
9830
+ disarm();
9831
+ };
9832
+ state.container?.addEventListener("touchcancel", on_touch_cancel);
9760
9833
  graph.on(CommonEvent.POINTER_DOWN, on_down);
9761
9834
  graph.on(CommonEvent.POINTER_MOVE, on_move);
9762
- graph.on(CommonEvent.POINTER_UP, cancel);
9835
+ graph.on(CommonEvent.POINTER_UP, on_up);
9763
9836
  return () => {
9764
- cancel();
9837
+ disarm();
9838
+ state.container?.removeEventListener("touchcancel", on_touch_cancel);
9765
9839
  graph.off(CommonEvent.POINTER_DOWN, on_down);
9766
9840
  graph.off(CommonEvent.POINTER_MOVE, on_move);
9767
- graph.off(CommonEvent.POINTER_UP, cancel);
9841
+ graph.off(CommonEvent.POINTER_UP, on_up);
9768
9842
  };
9769
9843
  }
9770
9844
  //#endregion
@@ -47964,6 +48038,7 @@ function configure_events(gobj) {
47964
48038
  let priv = gobj.priv;
47965
48039
  let graph = priv.graph;
47966
48040
  graph.on(CanvasEvent.CLICK, (evt) => {
48041
+ if (consume_long_press_click(graph)) return;
47967
48042
  gobj_send_event(gobj, "EV_CANVAS_CLICK", { evt }, gobj);
47968
48043
  });
47969
48044
  graph.on(NodeEvent.DRAG, (evt) => {
@@ -47977,12 +48052,14 @@ function configure_events(gobj) {
47977
48052
  gobj_send_event(gobj, "EV_NODE_DRAG_END", { evt }, gobj);
47978
48053
  });
47979
48054
  graph.on(NodeEvent.CLICK, (evt) => {
48055
+ if (consume_long_press_click(graph)) return;
47980
48056
  gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
47981
48057
  });
47982
48058
  graph.on(NodeEvent.CONTEXT_MENU, (evt) => {
47983
48059
  gobj_send_event(gobj, "EV_NODE_CONTEXT_MENU", { evt }, gobj);
47984
48060
  });
47985
48061
  graph.on(EdgeEvent.CLICK, (evt) => {
48062
+ if (consume_long_press_click(graph)) return;
47986
48063
  gobj_send_event(gobj, "EV_EDGE_CLICK", { evt }, gobj);
47987
48064
  });
47988
48065
  graph.on("aftertransform", () => {
@@ -48046,6 +48123,9 @@ function configure_plugins(gobj) {
48046
48123
  update_toolbar(gobj);
48047
48124
  }
48048
48125
  });
48126
+ let menu_enable = (e) => {
48127
+ return e.targetType === "node" || e.targetType === "edge";
48128
+ };
48049
48129
  graph_add_plugin(gobj, "contextmenu", {
48050
48130
  trigger: "contextmenu",
48051
48131
  onClick: (value) => {
@@ -48054,12 +48134,10 @@ function configure_plugins(gobj) {
48054
48134
  getItems: (e) => {
48055
48135
  return build_context_menu_items(gobj, e);
48056
48136
  },
48057
- enable: (e) => {
48058
- return e.targetType === "node" || e.targetType === "edge";
48059
- }
48137
+ enable: menu_enable
48060
48138
  });
48061
48139
  if (priv._uninstall_long_press) priv._uninstall_long_press();
48062
- priv._uninstall_long_press = install_long_press_contextmenu(priv.graph);
48140
+ priv._uninstall_long_press = install_long_press_contextmenu(priv.graph, { enable: menu_enable });
48063
48141
  if (gobj_read_bool_attr(gobj, "with_toolbar")) {
48064
48142
  configure_toolbar(gobj);
48065
48143
  configure_toolbar_edit(gobj);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.13",
3
+ "version": "7.23.14",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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: (e) => {
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(priv.graph);
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);
@@ -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} 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,45 @@ 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 state = touch_state_of(graph);
311
- let pending = null; /* {timer, x, y, event} */
373
+ let enable = options?.enable;
374
+ let state = touch_state_of(graph);
375
+ let pending = null; /* {x, y, t0, event} */
312
376
 
313
- let cancel = () => {
314
- if(pending) {
315
- clearTimeout(pending.timer);
316
- pending = null;
317
- }
377
+ let disarm = () => {
378
+ pending = null;
318
379
  };
319
380
 
320
381
  let fire = (event) => {
321
- pending = null;
382
+ /*
383
+ * The click @antv/g is about to make out of this same
384
+ * pointerup belongs to the press, and the press has already
385
+ * been spent on the menu.
386
+ */
387
+ state.long_press_click = true;
388
+ setTimeout(() => {
389
+ state.long_press_click = false;
390
+ }, 0);
322
391
 
323
392
  /*
324
- * The tap that ENDS the long press would reach the plugin's
325
- * own `document` click listener and hide the menu the press
326
- * just opened -- a right click never produces a `click`, so
327
- * the plugin has no reason to guard against it. Swallow
328
- * that one click in the CAPTURE phase, where document sees
329
- * it before anything else does.
393
+ * That click also reaches the plugin's own `document`
394
+ * listener, which hides the menu the press just opened -- a
395
+ * right click never produces a `click`, so the plugin has no
396
+ * reason to guard against it. Swallow that one click in the
397
+ * CAPTURE phase, where document sees it before anything else
398
+ * does.
330
399
  */
331
400
  let swallow = (evt) => {
332
401
  evt.stopPropagation();
@@ -337,30 +406,21 @@ export function install_long_press_contextmenu(graph)
337
406
  document.removeEventListener("click", swallow, true);
338
407
  }, 1000);
339
408
 
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
409
  graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
353
410
  graph.emit(CommonEvent.CONTEXT_MENU, event);
354
411
  };
355
412
 
356
413
  let on_down = (event) => {
357
- cancel();
414
+ disarm();
358
415
  if(event.pointerType !== "touch") {
359
416
  return; /* a mouse holding its button is not a menu */
360
417
  }
361
- if(state.count > 1) {
418
+ if(state.count > 1 || state.multi) {
362
419
  return; /* two fingers: that is the pinch */
363
420
  }
421
+ if(enable && !enable(event)) {
422
+ return; /* nothing here has a menu to open */
423
+ }
364
424
  let x = event.client?.x;
365
425
  let y = event.client?.y;
366
426
  if(x === undefined || y === undefined) {
@@ -368,16 +428,15 @@ export function install_long_press_contextmenu(graph)
368
428
  }
369
429
  /*
370
430
  * The event object is reused by @antv/g between dispatches,
371
- * so what the timer must carry is a COPY -- by the time it
372
- * fires, the live object describes the pointerup.
431
+ * so what the release must answer with is a COPY -- by then
432
+ * the live object describes the pointerup, and the menu is
433
+ * about what was PRESSED.
373
434
  */
374
- let frozen = Object.assign({}, event);
375
435
  pending = {
376
- x: x,
377
- y: y,
378
- timer: setTimeout(() => {
379
- fire(frozen);
380
- }, LONG_PRESS_MS),
436
+ x: x,
437
+ y: y,
438
+ t0: Date.now(),
439
+ event: Object.assign({}, event),
381
440
  };
382
441
  };
383
442
 
@@ -385,8 +444,8 @@ export function install_long_press_contextmenu(graph)
385
444
  if(!pending) {
386
445
  return;
387
446
  }
388
- if(state.count > 1) {
389
- cancel();
447
+ if(state.count > 1 || state.multi) {
448
+ disarm();
390
449
  return;
391
450
  }
392
451
  let x = event.client?.x;
@@ -394,20 +453,51 @@ export function install_long_press_contextmenu(graph)
394
453
  if(x === undefined || y === undefined) {
395
454
  return;
396
455
  }
397
- if(Math.abs(x - pending.x) > LONG_PRESS_SLOP ||
398
- Math.abs(y - pending.y) > LONG_PRESS_SLOP) {
399
- cancel();
456
+ if(press_moved(pending, x, y)) {
457
+ disarm(); /* the finger is carrying the node */
458
+ }
459
+ };
460
+
461
+ let on_up = (event) => {
462
+ let press = pending;
463
+ pending = null;
464
+ if(!press) {
465
+ return;
466
+ }
467
+ if(state.multi) {
468
+ return; /* a finger of a pinch, not a press */
469
+ }
470
+ let x = event.client?.x;
471
+ let y = event.client?.y;
472
+ if(x === undefined) {
473
+ x = press.x;
474
+ }
475
+ if(y === undefined) {
476
+ y = press.y;
400
477
  }
478
+ if(classify_press(press, x, y, Date.now()) !== "long") {
479
+ return; /* a drag, or a tap: both are already served */
480
+ }
481
+ fire(press.event);
482
+ };
483
+
484
+ /* The gesture can also end without a release: the browser takes
485
+ * the touch away (a system gesture, a call), and the press must
486
+ * not survive it into the next one. */
487
+ let on_touch_cancel = () => {
488
+ disarm();
401
489
  };
490
+ state.container?.addEventListener("touchcancel", on_touch_cancel);
402
491
 
403
492
  graph.on(CommonEvent.POINTER_DOWN, on_down);
404
493
  graph.on(CommonEvent.POINTER_MOVE, on_move);
405
- graph.on(CommonEvent.POINTER_UP, cancel);
494
+ graph.on(CommonEvent.POINTER_UP, on_up);
406
495
 
407
496
  return () => {
408
- cancel();
497
+ disarm();
498
+ state.container?.removeEventListener("touchcancel", on_touch_cancel);
409
499
  graph.off(CommonEvent.POINTER_DOWN, on_down);
410
500
  graph.off(CommonEvent.POINTER_MOVE, on_move);
411
- graph.off(CommonEvent.POINTER_UP, cancel);
501
+ graph.off(CommonEvent.POINTER_UP, on_up);
412
502
  };
413
503
  }
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
+ });