@yuneta/gobj-ui 7.23.7 → 7.23.9
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 +53 -0
- package/dist/gobj-ui.cjs.js +588 -112
- package/dist/gobj-ui.es.js +589 -113
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +415 -55
- package/src/c_yui_gobj_tree_js.js +2 -0
- package/src/c_yui_json.js +9 -3
- package/src/c_yui_json_graph.js +43 -23
- package/src/c_yui_treedb_graph.js +28 -18
- package/src/g6_drag_canvas_touch.js +21 -0
- package/src/g6_touch_gestures.js +413 -0
- package/src/lib_graph.css +19 -0
- package/src/lib_icons.js +14 -0
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
} from '@antv/g6';
|
|
60
60
|
|
|
61
61
|
import { ensure_drag_canvas_patch } from "./g6_drag_canvas_touch.js";
|
|
62
|
+
import { ensure_pinch_zoom_patch } from "./g6_touch_gestures.js";
|
|
62
63
|
import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
|
|
63
64
|
|
|
64
65
|
/***************************************************************
|
|
@@ -305,6 +306,7 @@ function mt_create(gobj)
|
|
|
305
306
|
build_ui(gobj);
|
|
306
307
|
|
|
307
308
|
ensure_drag_canvas_patch();
|
|
309
|
+
ensure_pinch_zoom_patch();
|
|
308
310
|
|
|
309
311
|
if(!__layout_registered__) {
|
|
310
312
|
register(ExtensionCategory.LAYOUT, 'gobj-tree-v', GobjTreeVLayout);
|
package/src/c_yui_json.js
CHANGED
|
@@ -365,14 +365,20 @@ function make_toolbar(gobj)
|
|
|
365
365
|
$search_input,
|
|
366
366
|
['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
|
|
367
367
|
]]);
|
|
368
|
+
/* The GLOBAL fold leads the row, ahead of the find box — the same
|
|
369
|
+
* place both graphs put theirs. It opens the whole document; the
|
|
370
|
+
* per-row toggles live in the rows. */
|
|
371
|
+
left_items.push(
|
|
372
|
+
icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
|
|
373
|
+
icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all")
|
|
374
|
+
);
|
|
375
|
+
|
|
368
376
|
attach_clear($search_control, $search_input);
|
|
369
377
|
left_items.push($search_control);
|
|
370
378
|
|
|
371
379
|
let right_items = [
|
|
372
380
|
view_mode_switch(gobj),
|
|
373
|
-
icon_button(gobj, "yi-
|
|
374
|
-
icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"),
|
|
375
|
-
icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
|
|
381
|
+
icon_button(gobj, "yi-copy", "EV_COPY_ALL", "copy json"),
|
|
376
382
|
];
|
|
377
383
|
|
|
378
384
|
const $toolbar = yui_toolbar({}, [
|
package/src/c_yui_json_graph.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from "@yuneta/gobj-js";
|
|
48
48
|
|
|
49
49
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
50
|
+
import {attach_clear} from "./yui_inputs.js";
|
|
50
51
|
import {
|
|
51
52
|
yui_graph_camera_items,
|
|
52
53
|
yui_graph_fold_items,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
} from '@antv/g6';
|
|
67
68
|
|
|
68
69
|
import { ensure_drag_canvas_patch } from "./g6_drag_canvas_touch.js";
|
|
70
|
+
import { ensure_pinch_zoom_patch } from "./g6_touch_gestures.js";
|
|
69
71
|
import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
|
|
70
72
|
|
|
71
73
|
/***************************************************************
|
|
@@ -300,6 +302,7 @@ function mt_create(gobj)
|
|
|
300
302
|
build_ui(gobj);
|
|
301
303
|
|
|
302
304
|
ensure_drag_canvas_patch();
|
|
305
|
+
ensure_pinch_zoom_patch();
|
|
303
306
|
|
|
304
307
|
if(!__layout_registered__) {
|
|
305
308
|
register(ExtensionCategory.LAYOUT, 'json-tree-layout', JsonTreeLayout);
|
|
@@ -478,27 +481,25 @@ function make_toolbar(gobj)
|
|
|
478
481
|
* looks the same whether nothing matched or the match was already
|
|
479
482
|
* on screen.
|
|
480
483
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
], {
|
|
484
|
+
/* Materialised, not a spec: attach_clear() hangs the NORM clear (✕)
|
|
485
|
+
* on a real element. Clearing dispatches a synthetic `input`, which
|
|
486
|
+
* goes through the same rate-limited handler and fires EV_FIND_NODES
|
|
487
|
+
* with an empty term — the box and the highlight clear together. */
|
|
488
|
+
let $find_input = createElement2(
|
|
489
|
+
['input', {
|
|
490
|
+
class: 'JSON_GRAPH_FIND_INPUT input',
|
|
491
|
+
type: 'text',
|
|
492
|
+
/* A placeholder is not a text node, so the data-i18n walk
|
|
493
|
+
* cannot reach it: it needs its own key. */
|
|
494
|
+
placeholder: t('search'),
|
|
495
|
+
'data-i18n-placeholder': 'search',
|
|
496
|
+
'aria-label': t('search'),
|
|
497
|
+
'data-i18n-aria-label': 'search'
|
|
498
|
+
}, [], {
|
|
496
499
|
/* Rate-limited, not delayed for effect: every keystroke
|
|
497
500
|
* REBUILDS the cards — an html node paints no G6 state, so
|
|
498
501
|
* the highlight has to live in the card's own markup — and
|
|
499
|
-
* the first letter typed can match most of a document.
|
|
500
|
-
* Input plumbing: the action still crosses the FSM, just
|
|
501
|
-
* not once per keystroke. */
|
|
502
|
+
* the first letter typed can match most of a document. */
|
|
502
503
|
input: (evt) => {
|
|
503
504
|
evt.stopPropagation();
|
|
504
505
|
let text = evt.target.value.trim();
|
|
@@ -510,13 +511,32 @@ function make_toolbar(gobj)
|
|
|
510
511
|
gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
|
|
511
512
|
}, 250);
|
|
512
513
|
}
|
|
513
|
-
}]
|
|
514
|
+
}]);
|
|
515
|
+
|
|
516
|
+
let $find_control = createElement2(
|
|
517
|
+
['div', {class: 'JSON_GRAPH_FIND control has-icons-left',
|
|
518
|
+
style: 'margin-right:.5rem; max-width:12rem; min-width:7rem;'}, [
|
|
519
|
+
$find_input,
|
|
520
|
+
['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
|
|
521
|
+
]]);
|
|
522
|
+
attach_clear($find_control, $find_input);
|
|
523
|
+
|
|
524
|
+
/*
|
|
525
|
+
* Left: the global fold leads the row, then find a node.
|
|
526
|
+
*
|
|
527
|
+
* A graph of a large document has no other way in — the only way to
|
|
528
|
+
* locate a key was to read every card. The count SAYS how many
|
|
529
|
+
* matched, because a graph that did not move looks the same whether
|
|
530
|
+
* nothing matched or the match was already on screen.
|
|
531
|
+
*/
|
|
532
|
+
let left_items = yui_graph_fold_items(gobj, toolbar_wide).concat([
|
|
533
|
+
$find_control,
|
|
514
534
|
/* Two spans, not one string: the number is DATA and "matches"
|
|
515
535
|
* is the word, so a language switch re-translates the half that
|
|
516
|
-
* is a word.
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
536
|
+
* is a word. `display:flex` inline and NOT the `is-flex`
|
|
537
|
+
* helper — both Bulma helpers carry !important, so
|
|
538
|
+
* `is-hidden is-flex` on one element is decided by stylesheet
|
|
539
|
+
* order. */
|
|
520
540
|
['div', {class: 'JSON_GRAPH_FIND_RESULT is-hidden',
|
|
521
541
|
style: 'display:flex; align-items:center; gap:.3rem; ' +
|
|
522
542
|
'margin-right:.5rem; font-size:.85rem;'}, [
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
} from "@yuneta/gobj-js";
|
|
79
79
|
|
|
80
80
|
import {yui_toolbar} from "./yui_toolbar.js";
|
|
81
|
+
import {attach_clear} from "./yui_inputs.js";
|
|
81
82
|
import {register_c_g6_nodes_tree} from "./c_g6_nodes_tree.js";
|
|
82
83
|
import {
|
|
83
84
|
removeChildElements,
|
|
@@ -548,23 +549,21 @@ function make_toolbar(gobj)
|
|
|
548
549
|
* not move looks the same whether nothing matched or the match was
|
|
549
550
|
* already on screen.
|
|
550
551
|
*/
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
]]
|
|
567
|
-
], {
|
|
552
|
+
/* Materialised, not a spec: attach_clear() hangs the NORM clear (✕)
|
|
553
|
+
* on a real element. Clearing dispatches a synthetic `input`, which
|
|
554
|
+
* goes through the same rate-limited handler and fires EV_FIND_NODES
|
|
555
|
+
* with an empty term — the box and the highlight clear together. */
|
|
556
|
+
let $find_input = createElement2(
|
|
557
|
+
['input', {
|
|
558
|
+
class: 'GRAPH_FIND_INPUT input',
|
|
559
|
+
type: 'text',
|
|
560
|
+
/* A placeholder is not a text node, so the data-i18n walk
|
|
561
|
+
* cannot reach it: it needs its own key. */
|
|
562
|
+
placeholder: t('search'),
|
|
563
|
+
'data-i18n-placeholder': 'search',
|
|
564
|
+
'aria-label': t('search'),
|
|
565
|
+
'data-i18n-aria-label': 'search'
|
|
566
|
+
}, [], {
|
|
568
567
|
/* Rate-limited, not delayed for effect: a match repaints the
|
|
569
568
|
* cards it lands on, and on a large treedb the first letter
|
|
570
569
|
* typed can match hundreds. This is input plumbing — the
|
|
@@ -581,7 +580,18 @@ function make_toolbar(gobj)
|
|
|
581
580
|
gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
|
|
582
581
|
}, 250);
|
|
583
582
|
}
|
|
584
|
-
}]
|
|
583
|
+
}]);
|
|
584
|
+
|
|
585
|
+
let $find_control = createElement2(
|
|
586
|
+
['div', {class: 'GRAPH_FIND control has-icons-left',
|
|
587
|
+
style: 'margin-right:.5rem; max-width:12rem; min-width:7rem;'}, [
|
|
588
|
+
$find_input,
|
|
589
|
+
['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
|
|
590
|
+
]]);
|
|
591
|
+
attach_clear($find_control, $find_input);
|
|
592
|
+
|
|
593
|
+
let center_items = [
|
|
594
|
+
$find_control,
|
|
585
595
|
/* Two spans, not one string: the number is DATA and "matches" is
|
|
586
596
|
* the word, so a language switch re-translates the half that is a
|
|
587
597
|
* word. Spaced with CSS because createElement2 trims text nodes. */
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
* every graph (tree, json, treedb, editor) is fixed without
|
|
42
42
|
* touching each consumer's behaviors list.
|
|
43
43
|
*
|
|
44
|
+
* It also has to keep the ONE guard the base class has and
|
|
45
|
+
* this replacement dropped: G6's `onDrag` refuses to pan
|
|
46
|
+
* while a pinch is in progress. See `is_pinching` in
|
|
47
|
+
* `g6_touch_gestures.js`, which is where the pinch that
|
|
48
|
+
* matters is now recognised.
|
|
49
|
+
*
|
|
44
50
|
* Copyright (c) 2026, ArtGins.
|
|
45
51
|
* All Rights Reserved.
|
|
46
52
|
***********************************************************************/
|
|
@@ -51,6 +57,8 @@ import {
|
|
|
51
57
|
register,
|
|
52
58
|
} from '@antv/g6';
|
|
53
59
|
|
|
60
|
+
import {is_pinching} from './g6_touch_gestures.js';
|
|
61
|
+
|
|
54
62
|
/************************************************************
|
|
55
63
|
* Custom drag-canvas: same behavior, reliable delta source
|
|
56
64
|
************************************************************/
|
|
@@ -74,6 +82,19 @@ class DragCanvasTouch extends DragCanvas {
|
|
|
74
82
|
this._lastVP = null;
|
|
75
83
|
return;
|
|
76
84
|
}
|
|
85
|
+
/*
|
|
86
|
+
* Two fingers on the glass is a PINCH, not a pan. G6's
|
|
87
|
+
* own onDrag guards on `PinchHandler.isPinching` and
|
|
88
|
+
* this replacement dropped it, which cost nothing while
|
|
89
|
+
* no graph answered the pinch -- and would have panned
|
|
90
|
+
* the canvas out from under the zoom the day one did.
|
|
91
|
+
* `null` so the next one-finger frame re-seeds instead
|
|
92
|
+
* of jumping the camera by the whole gesture.
|
|
93
|
+
*/
|
|
94
|
+
if(is_pinching(this.context.graph)) {
|
|
95
|
+
this._lastVP = null;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
77
98
|
let vp = event.viewport;
|
|
78
99
|
if(!vp || typeof vp.x !== "number" || typeof vp.y !== "number") {
|
|
79
100
|
return;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* g6_touch_gestures.js
|
|
3
|
+
*
|
|
4
|
+
* The two gestures a G6 graph needs on a touch screen and
|
|
5
|
+
* does not have: PINCH to zoom, and LONG PRESS to open the
|
|
6
|
+
* context menu.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists
|
|
9
|
+
* ----------------
|
|
10
|
+
* G6 v5 draws a graph that reads fine on a phone and cannot
|
|
11
|
+
* be OPERATED on one. Two gaps, both structural:
|
|
12
|
+
*
|
|
13
|
+
* 1. `zoom-canvas` binds the WHEEL and nothing else
|
|
14
|
+
* (`trigger: []`). There is no wheel on a phone, so the
|
|
15
|
+
* only zoom is the toolbar's +/- buttons. G6 does ship a
|
|
16
|
+
* pinch recogniser, and `zoom-canvas` will use it -- with
|
|
17
|
+
* `trigger: ['pinch']`, which its `bindEvents` treats as
|
|
18
|
+
* an ALTERNATIVE to the wheel (`if pinch ... else wheel`),
|
|
19
|
+
* so opting in costs the mouse. Worse, its `PinchHandler`
|
|
20
|
+
* keeps its instance and its callback list in STATICS: on
|
|
21
|
+
* a page with two graphs the second one registers its
|
|
22
|
+
* callback against the FIRST one's emitter, so pinching
|
|
23
|
+
* graph A zooms both and pinching graph B does nothing.
|
|
24
|
+
* We recognise the gesture ourselves, per graph.
|
|
25
|
+
*
|
|
26
|
+
* 2. The context menu is bound to `contextmenu`, and G6 does
|
|
27
|
+
* not take that from the DOM: its `BehaviorController`
|
|
28
|
+
* SYNTHESISES it from `pointerdown` with `button === 2`.
|
|
29
|
+
* A long press therefore never reaches the plugin on ANY
|
|
30
|
+
* platform, whatever the browser does with the gesture.
|
|
31
|
+
* So we synthesise it the same way G6 does, from the same
|
|
32
|
+
* forwarded event object -- which is why the plugin's
|
|
33
|
+
* `getItems(e)` receives exactly the shape it expects.
|
|
34
|
+
*
|
|
35
|
+
* Both are built on ONE per-graph record of the fingers
|
|
36
|
+
* on the glass, read from the NATIVE touch events -- see
|
|
37
|
+
* `touch_state_of` for why the pointer stream, which looks
|
|
38
|
+
* like the right source, is not. That record is also what
|
|
39
|
+
* tells `drag-canvas` to stop panning while two fingers are
|
|
40
|
+
* down (`is_pinching`), the guard G6's own drag-canvas has
|
|
41
|
+
* and our replacement dropped.
|
|
42
|
+
*
|
|
43
|
+
* Copyright (c) 2026, ArtGins.
|
|
44
|
+
* All Rights Reserved.
|
|
45
|
+
***********************************************************************/
|
|
46
|
+
import {
|
|
47
|
+
ZoomCanvas,
|
|
48
|
+
CommonEvent,
|
|
49
|
+
ExtensionCategory,
|
|
50
|
+
register,
|
|
51
|
+
} from '@antv/g6';
|
|
52
|
+
|
|
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 */
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/***************************************************************
|
|
69
|
+
* Per-graph record of the fingers on the glass
|
|
70
|
+
***************************************************************/
|
|
71
|
+
const __touch_state__ = new WeakMap();
|
|
72
|
+
|
|
73
|
+
/************************************************************
|
|
74
|
+
* The touch record of one graph, installing its listeners
|
|
75
|
+
* the first time it is asked for.
|
|
76
|
+
*
|
|
77
|
+
* Read from the NATIVE touch events, not from G6's
|
|
78
|
+
* forwarded pointer stream.
|
|
79
|
+
*
|
|
80
|
+
* The pointer stream looks like the right source and is
|
|
81
|
+
* not: @antv/g re-issues pointer ids in the middle of a
|
|
82
|
+
* two-finger gesture (measured: a pinch that started on
|
|
83
|
+
* ids 2 and 3 finished on id 1), so anything that keys a
|
|
84
|
+
* Map on `pointerId` loses one of the two fingers halfway
|
|
85
|
+
* and reads the gesture as a fraction of what it was.
|
|
86
|
+
* G6's own `PinchHandler` is built exactly that way.
|
|
87
|
+
*
|
|
88
|
+
* `event.touches` needs no bookkeeping at all: it IS the
|
|
89
|
+
* list of fingers currently down, re-stated on every event.
|
|
90
|
+
************************************************************/
|
|
91
|
+
function touch_state_of(graph)
|
|
92
|
+
{
|
|
93
|
+
let state = __touch_state__.get(graph);
|
|
94
|
+
if(state) {
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
state = {
|
|
99
|
+
count: 0, /* fingers on the glass right now */
|
|
100
|
+
container: null,
|
|
101
|
+
};
|
|
102
|
+
__touch_state__.set(graph, state);
|
|
103
|
+
|
|
104
|
+
let container = graph.getCanvas?.()?.getContainer?.();
|
|
105
|
+
if(!container) {
|
|
106
|
+
return state; /* no DOM yet: nothing to listen to */
|
|
107
|
+
}
|
|
108
|
+
state.container = container;
|
|
109
|
+
|
|
110
|
+
let read = (event) => {
|
|
111
|
+
state.count = event.touches? event.touches.length: 0;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/* Passive: we never preventDefault here -- the canvas already
|
|
115
|
+
* carries `touch-action: none`, which is what actually stops
|
|
116
|
+
* the browser from panning the page under the graph. */
|
|
117
|
+
container.addEventListener("touchstart", read, {passive: true});
|
|
118
|
+
container.addEventListener("touchmove", read, {passive: true});
|
|
119
|
+
container.addEventListener("touchend", read, {passive: true});
|
|
120
|
+
container.addEventListener("touchcancel", read, {passive: true});
|
|
121
|
+
|
|
122
|
+
return state;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/************************************************************
|
|
126
|
+
* Is a two-finger gesture in progress on this graph?
|
|
127
|
+
*
|
|
128
|
+
* Read by our `drag-canvas` replacement: panning and zooming
|
|
129
|
+
* the same drag at the same time fights the user for the
|
|
130
|
+
* camera.
|
|
131
|
+
************************************************************/
|
|
132
|
+
export function is_pinching(graph)
|
|
133
|
+
{
|
|
134
|
+
if(!graph) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
let state = __touch_state__.get(graph);
|
|
138
|
+
if(!state) {
|
|
139
|
+
return false; /* no touch gesture was ever installed here */
|
|
140
|
+
}
|
|
141
|
+
return state.count >= 2;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
/***************************
|
|
148
|
+
* Pinch to zoom
|
|
149
|
+
***************************/
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
/************************************************************
|
|
155
|
+
* `zoom-canvas` that also answers the pinch.
|
|
156
|
+
*
|
|
157
|
+
* The base class keeps the wheel; the pinch is added on top
|
|
158
|
+
* instead of replacing it, so one build serves the mouse and
|
|
159
|
+
* the finger. The scale is ABSOLUTE against the zoom the
|
|
160
|
+
* gesture started at -- G6's own pinch feeds `zoom()` an
|
|
161
|
+
* increment per frame, which drifts away from the fingers
|
|
162
|
+
* over a long gesture.
|
|
163
|
+
************************************************************/
|
|
164
|
+
class ZoomCanvasPinch extends ZoomCanvas
|
|
165
|
+
{
|
|
166
|
+
constructor(context, options)
|
|
167
|
+
{
|
|
168
|
+
super(context, options);
|
|
169
|
+
|
|
170
|
+
this._pinch_dist0 = 0;
|
|
171
|
+
this._pinch_zoom0 = 1;
|
|
172
|
+
this._on_touch_move = null;
|
|
173
|
+
|
|
174
|
+
let {trigger} = this.options;
|
|
175
|
+
if(Array.isArray(trigger) && trigger.includes(CommonEvent.PINCH)) {
|
|
176
|
+
return; /* the caller asked for G6's own pinch */
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let graph = this.context.graph;
|
|
180
|
+
let state = touch_state_of(graph);
|
|
181
|
+
if(!state.container) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this._on_touch_move = (event) => {
|
|
186
|
+
let touches = event.touches;
|
|
187
|
+
if(!touches || touches.length !== 2) {
|
|
188
|
+
/* Not two fingers (yet, or any more): the next pinch
|
|
189
|
+
* measures itself afresh. */
|
|
190
|
+
this._pinch_dist0 = 0;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let dx = touches[0].clientX - touches[1].clientX;
|
|
194
|
+
let dy = touches[0].clientY - touches[1].clientY;
|
|
195
|
+
let dist = Math.sqrt(dx * dx + dy * dy);
|
|
196
|
+
if(dist <= 0) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if(this._pinch_dist0 === 0) {
|
|
200
|
+
/* First frame of this gesture: remember what it is
|
|
201
|
+
* measured against. */
|
|
202
|
+
this._pinch_dist0 = dist;
|
|
203
|
+
this._pinch_zoom0 = graph.getZoom();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this._pinch_to(dist / this._pinch_dist0, touches, event);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/*
|
|
210
|
+
* Bound on the CONTAINER and not through `bindEvents()`,
|
|
211
|
+
* which `update()` runs again on every option change: the
|
|
212
|
+
* wheel goes through `Shortcut`, which unbinds itself
|
|
213
|
+
* first, and a listener put straight on an element would
|
|
214
|
+
* simply pile up.
|
|
215
|
+
*/
|
|
216
|
+
state.container.addEventListener(
|
|
217
|
+
"touchmove", this._on_touch_move, {passive: true}
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/************************************************************
|
|
222
|
+
* Zoom to `scale` times the zoom the pinch started at,
|
|
223
|
+
* about the midpoint between the two fingers.
|
|
224
|
+
*
|
|
225
|
+
* ABSOLUTE against the start of the gesture, where G6's
|
|
226
|
+
* own pinch feeds `zoom()` an increment per frame: an
|
|
227
|
+
* increment drifts away from the fingers over a long
|
|
228
|
+
* gesture, and cannot come back when they close again.
|
|
229
|
+
************************************************************/
|
|
230
|
+
_pinch_to(scale, touches, event)
|
|
231
|
+
{
|
|
232
|
+
if(!this.validate(event)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let graph = this.context.graph;
|
|
236
|
+
let state = touch_state_of(graph);
|
|
237
|
+
let origin = null;
|
|
238
|
+
|
|
239
|
+
/* The camera's origin is measured in the canvas's own
|
|
240
|
+
* viewport space, which is the container box: the canvas
|
|
241
|
+
* fills it, so the offset IS the container's corner. */
|
|
242
|
+
let rect = state.container?.getBoundingClientRect();
|
|
243
|
+
if(rect) {
|
|
244
|
+
origin = [
|
|
245
|
+
(touches[0].clientX + touches[1].clientX) / 2 - rect.left,
|
|
246
|
+
(touches[0].clientY + touches[1].clientY) / 2 - rect.top,
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* `zoomTo` clamps to the graph's own `zoomRange`; no
|
|
251
|
+
* animation, the fingers are the animation. */
|
|
252
|
+
graph.zoomTo(this._pinch_zoom0 * scale, false, origin);
|
|
253
|
+
this.options.onFinish?.();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
destroy()
|
|
257
|
+
{
|
|
258
|
+
if(this._on_touch_move) {
|
|
259
|
+
let state = __touch_state__.get(this.context.graph);
|
|
260
|
+
state?.container?.removeEventListener("touchmove", this._on_touch_move);
|
|
261
|
+
this._on_touch_move = null;
|
|
262
|
+
}
|
|
263
|
+
super.destroy();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/************************************************************
|
|
268
|
+
* Register once, over the built-in `zoom-canvas`, so every
|
|
269
|
+
* graph (tree, json, treedb, editor) pinches without
|
|
270
|
+
* touching its behaviors list.
|
|
271
|
+
************************************************************/
|
|
272
|
+
let __zoom_canvas_patched__ = false;
|
|
273
|
+
|
|
274
|
+
export function ensure_pinch_zoom_patch()
|
|
275
|
+
{
|
|
276
|
+
if(__zoom_canvas_patched__) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
register(ExtensionCategory.BEHAVIOR, 'zoom-canvas', ZoomCanvasPinch);
|
|
280
|
+
__zoom_canvas_patched__ = true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
/***************************
|
|
287
|
+
* Long press
|
|
288
|
+
***************************/
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
/************************************************************
|
|
294
|
+
* Make a long press open the context menu.
|
|
295
|
+
*
|
|
296
|
+
* Returns the function that uninstalls it.
|
|
297
|
+
*
|
|
298
|
+
* The menu the press opens is whatever the `contextmenu`
|
|
299
|
+
* plugin builds: we re-emit G6's own forwarded event under
|
|
300
|
+
* the name the plugin listens for, so `getItems(e)` and
|
|
301
|
+
* `enable(e)` see the same object a right-click would give
|
|
302
|
+
* them -- same `target`, same `targetType`, same `client`.
|
|
303
|
+
************************************************************/
|
|
304
|
+
export function install_long_press_contextmenu(graph)
|
|
305
|
+
{
|
|
306
|
+
if(!graph) {
|
|
307
|
+
return () => {};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let state = touch_state_of(graph);
|
|
311
|
+
let pending = null; /* {timer, x, y, event} */
|
|
312
|
+
|
|
313
|
+
let cancel = () => {
|
|
314
|
+
if(pending) {
|
|
315
|
+
clearTimeout(pending.timer);
|
|
316
|
+
pending = null;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
let fire = (event) => {
|
|
321
|
+
pending = null;
|
|
322
|
+
|
|
323
|
+
/*
|
|
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.
|
|
330
|
+
*/
|
|
331
|
+
let swallow = (evt) => {
|
|
332
|
+
evt.stopPropagation();
|
|
333
|
+
document.removeEventListener("click", swallow, true);
|
|
334
|
+
};
|
|
335
|
+
document.addEventListener("click", swallow, true);
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
document.removeEventListener("click", swallow, true);
|
|
338
|
+
}, 1000);
|
|
339
|
+
|
|
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
|
+
graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
|
|
353
|
+
graph.emit(CommonEvent.CONTEXT_MENU, event);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
let on_down = (event) => {
|
|
357
|
+
cancel();
|
|
358
|
+
if(event.pointerType !== "touch") {
|
|
359
|
+
return; /* a mouse holding its button is not a menu */
|
|
360
|
+
}
|
|
361
|
+
if(state.count > 1) {
|
|
362
|
+
return; /* two fingers: that is the pinch */
|
|
363
|
+
}
|
|
364
|
+
let x = event.client?.x;
|
|
365
|
+
let y = event.client?.y;
|
|
366
|
+
if(x === undefined || y === undefined) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
/*
|
|
370
|
+
* 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.
|
|
373
|
+
*/
|
|
374
|
+
let frozen = Object.assign({}, event);
|
|
375
|
+
pending = {
|
|
376
|
+
x: x,
|
|
377
|
+
y: y,
|
|
378
|
+
timer: setTimeout(() => {
|
|
379
|
+
fire(frozen);
|
|
380
|
+
}, LONG_PRESS_MS),
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
let on_move = (event) => {
|
|
385
|
+
if(!pending) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if(state.count > 1) {
|
|
389
|
+
cancel();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
let x = event.client?.x;
|
|
393
|
+
let y = event.client?.y;
|
|
394
|
+
if(x === undefined || y === undefined) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if(Math.abs(x - pending.x) > LONG_PRESS_SLOP ||
|
|
398
|
+
Math.abs(y - pending.y) > LONG_PRESS_SLOP) {
|
|
399
|
+
cancel();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
graph.on(CommonEvent.POINTER_DOWN, on_down);
|
|
404
|
+
graph.on(CommonEvent.POINTER_MOVE, on_move);
|
|
405
|
+
graph.on(CommonEvent.POINTER_UP, cancel);
|
|
406
|
+
|
|
407
|
+
return () => {
|
|
408
|
+
cancel();
|
|
409
|
+
graph.off(CommonEvent.POINTER_DOWN, on_down);
|
|
410
|
+
graph.off(CommonEvent.POINTER_MOVE, on_move);
|
|
411
|
+
graph.off(CommonEvent.POINTER_UP, cancel);
|
|
412
|
+
};
|
|
413
|
+
}
|