@flowgram-vue/core 0.2.0

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.
Files changed (133) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +0 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +0 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +63 -0
  8. package/src/common/config-entity.ts +80 -0
  9. package/src/common/entity-data.ts +171 -0
  10. package/src/common/entity-manager-contribution.ts +12 -0
  11. package/src/common/entity-manager.ts +475 -0
  12. package/src/common/entity.ts +482 -0
  13. package/src/common/index.ts +22 -0
  14. package/src/common/playground-context.ts +35 -0
  15. package/src/common/playground-decorator-helper.ts +113 -0
  16. package/src/common/playground-decorators.ts +85 -0
  17. package/src/common/playground-schedule.ts +30 -0
  18. package/src/common/protect-wheel-area.ts +11 -0
  19. package/src/common/schema/index.ts +14 -0
  20. package/src/common/schema/node.ts +15 -0
  21. package/src/common/schema/opacity-schema.ts +19 -0
  22. package/src/common/schema/origin-schema.ts +35 -0
  23. package/src/common/schema/position-schema.ts +35 -0
  24. package/src/common/schema/rotation-schema.ts +19 -0
  25. package/src/common/schema/scale-schema.ts +35 -0
  26. package/src/common/schema/size-schema.ts +42 -0
  27. package/src/common/schema/skew-schema.ts +35 -0
  28. package/src/common/schema/transform-schema.ts +613 -0
  29. package/src/common/utils/bounds.spec.ts +229 -0
  30. package/src/common/utils/bounds.ts +176 -0
  31. package/src/common/utils/index.ts +6 -0
  32. package/src/composables/index.ts +16 -0
  33. package/src/composables/use-config-entity.ts +31 -0
  34. package/src/composables/use-entities.ts +26 -0
  35. package/src/composables/use-entity-data-from-context.ts +34 -0
  36. package/src/composables/use-entity-from-context.ts +31 -0
  37. package/src/composables/use-listen-events.ts +22 -0
  38. package/src/composables/use-playground-container.ts +16 -0
  39. package/src/composables/use-playground-context.ts +15 -0
  40. package/src/composables/use-playground-drag.ts +31 -0
  41. package/src/composables/use-playground.ts +16 -0
  42. package/src/composables/use-refresh.ts +6 -0
  43. package/src/composables/use-service.ts +17 -0
  44. package/src/core/index.ts +8 -0
  45. package/src/core/layer/config/editor-state-config-entity.ts +180 -0
  46. package/src/core/layer/config/index.ts +7 -0
  47. package/src/core/layer/config/playground-config-entity.ts +604 -0
  48. package/src/core/layer/index.ts +8 -0
  49. package/src/core/layer/layer.ts +248 -0
  50. package/src/core/layer/playground-layer.ts +547 -0
  51. package/src/core/pipeline/index.ts +10 -0
  52. package/src/core/pipeline/pipeline-entities-selector.ts +200 -0
  53. package/src/core/pipeline/pipeline-entities.ts +230 -0
  54. package/src/core/pipeline/pipeline-registry.ts +344 -0
  55. package/src/core/pipeline/pipeline-renderer.ts +248 -0
  56. package/src/core/pipeline/pipeline-vue-utils.ts +96 -0
  57. package/src/core/pipeline/pipeline.ts +28 -0
  58. package/src/core/utils/index.ts +12 -0
  59. package/src/core/utils/inject-provider-decorators.ts +27 -0
  60. package/src/core/utils/lazy-inject-decorators.ts +38 -0
  61. package/src/core/utils/mouse-touch-event.ts +120 -0
  62. package/src/core/utils/playground-drag.ts +511 -0
  63. package/src/core/utils/playground-gesture.spec.ts +135 -0
  64. package/src/core/utils/playground-gesture.ts +118 -0
  65. package/src/core/utils/tween.ts +156 -0
  66. package/src/core/utils/use-gesture/core/Controller.ts +212 -0
  67. package/src/core/utils/use-gesture/core/EventStore.ts +46 -0
  68. package/src/core/utils/use-gesture/core/TimeoutStore.ts +28 -0
  69. package/src/core/utils/use-gesture/core/actions.ts +63 -0
  70. package/src/core/utils/use-gesture/core/config/commonConfigResolver.ts +86 -0
  71. package/src/core/utils/use-gesture/core/config/coordinatesConfigResolver.ts +48 -0
  72. package/src/core/utils/use-gesture/core/config/dragConfigResolver.ts +140 -0
  73. package/src/core/utils/use-gesture/core/config/hoverConfigResolver.ts +11 -0
  74. package/src/core/utils/use-gesture/core/config/moveConfigResolver.ts +11 -0
  75. package/src/core/utils/use-gesture/core/config/pinchConfigResolver.ts +59 -0
  76. package/src/core/utils/use-gesture/core/config/resolver.ts +70 -0
  77. package/src/core/utils/use-gesture/core/config/scrollConfigResolver.ts +8 -0
  78. package/src/core/utils/use-gesture/core/config/sharedConfigResolver.ts +28 -0
  79. package/src/core/utils/use-gesture/core/config/support.ts +52 -0
  80. package/src/core/utils/use-gesture/core/config/wheelConfigResolver.ts +8 -0
  81. package/src/core/utils/use-gesture/core/engines/CoordinatesEngine.ts +78 -0
  82. package/src/core/utils/use-gesture/core/engines/DragEngine.ts +403 -0
  83. package/src/core/utils/use-gesture/core/engines/Engine.ts +406 -0
  84. package/src/core/utils/use-gesture/core/engines/HoverEngine.ts +43 -0
  85. package/src/core/utils/use-gesture/core/engines/MoveEngine.ts +52 -0
  86. package/src/core/utils/use-gesture/core/engines/PinchEngine.ts +345 -0
  87. package/src/core/utils/use-gesture/core/engines/ScrollEngine.ts +42 -0
  88. package/src/core/utils/use-gesture/core/engines/WheelEngine.ts +48 -0
  89. package/src/core/utils/use-gesture/core/index.ts +7 -0
  90. package/src/core/utils/use-gesture/core/parser.ts +92 -0
  91. package/src/core/utils/use-gesture/core/types/action.ts +19 -0
  92. package/src/core/utils/use-gesture/core/types/config.ts +255 -0
  93. package/src/core/utils/use-gesture/core/types/handlers.ts +73 -0
  94. package/src/core/utils/use-gesture/core/types/index.ts +11 -0
  95. package/src/core/utils/use-gesture/core/types/internalConfig.ts +80 -0
  96. package/src/core/utils/use-gesture/core/types/state.ts +275 -0
  97. package/src/core/utils/use-gesture/core/types/utils.ts +204 -0
  98. package/src/core/utils/use-gesture/core/types.ts +8 -0
  99. package/src/core/utils/use-gesture/core/utils/events.ts +158 -0
  100. package/src/core/utils/use-gesture/core/utils/fn.ts +32 -0
  101. package/src/core/utils/use-gesture/core/utils/maths.ts +63 -0
  102. package/src/core/utils/use-gesture/core/utils/state.ts +24 -0
  103. package/src/core/utils/use-gesture/core/utils.ts +8 -0
  104. package/src/core/utils/use-gesture/index.ts +6 -0
  105. package/src/core/utils/use-gesture/vanilla/DragGesture.ts +27 -0
  106. package/src/core/utils/use-gesture/vanilla/Gesture.ts +45 -0
  107. package/src/core/utils/use-gesture/vanilla/HoverGesture.ts +27 -0
  108. package/src/core/utils/use-gesture/vanilla/MoveGesture.ts +27 -0
  109. package/src/core/utils/use-gesture/vanilla/PinchGesture.ts +27 -0
  110. package/src/core/utils/use-gesture/vanilla/Recognizer.ts +43 -0
  111. package/src/core/utils/use-gesture/vanilla/ScrollGesture.ts +27 -0
  112. package/src/core/utils/use-gesture/vanilla/WheelGesture.ts +27 -0
  113. package/src/core/utils/use-gesture/vanilla/createGesture.ts +18 -0
  114. package/src/core/utils/use-gesture/vanilla/index.ts +18 -0
  115. package/src/env.d.ts +10 -0
  116. package/src/index.ts +17 -0
  117. package/src/playground-config.ts +70 -0
  118. package/src/playground-container.ts +129 -0
  119. package/src/playground-contribution.ts +81 -0
  120. package/src/playground-mock-tools.ts +143 -0
  121. package/src/playground.ts +401 -0
  122. package/src/plugin/index.ts +6 -0
  123. package/src/plugin/plugin.ts +226 -0
  124. package/src/services/clipboard-service.ts +40 -0
  125. package/src/services/context-menu-service.ts +25 -0
  126. package/src/services/index.ts +10 -0
  127. package/src/services/logger-service.ts +49 -0
  128. package/src/services/selection-service.ts +54 -0
  129. package/src/services/storage-service.ts +65 -0
  130. package/src/vue/index.ts +8 -0
  131. package/src/vue/playground-vue-context.ts +22 -0
  132. package/src/vue/playground-vue-provider.ts +115 -0
  133. package/src/vue/playground-vue-renderer.vue +44 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { describe, it, expect, vi } from 'vitest';
7
+
8
+ import { PlaygroundGesture } from './playground-gesture';
9
+
10
+ vi.mock('@use-gesture/vanilla', () => {
11
+ class Gesture {}
12
+ return {
13
+ Gesture,
14
+ };
15
+ });
16
+
17
+ describe('PlaygroundGesture', () => {
18
+ const el = document.createElement('div');
19
+ const playgroundGesture = new PlaygroundGesture(el, {
20
+ config: {
21
+ minZoom: 0.4,
22
+ maxZoom: 2.2,
23
+ resolution: 1,
24
+ },
25
+ } as any);
26
+ it('getScaleBounds', () => {
27
+ expect(playgroundGesture.getScaleBounds()).toEqual({
28
+ min: 0.4,
29
+ max: 2.2,
30
+ });
31
+ });
32
+ it('pinching', () => {
33
+ expect(playgroundGesture.pinching).toBeFalsy();
34
+ });
35
+ });
36
+
37
+ describe('PlaygroundGesture', () => {
38
+ let el: HTMLElement;
39
+ let config: any;
40
+ let playgroundGesture: PlaygroundGesture;
41
+
42
+ beforeEach(() => {
43
+ el = document.createElement('div');
44
+ config = {
45
+ config: {
46
+ minZoom: 0.4,
47
+ maxZoom: 2.2,
48
+ resolution: 1,
49
+ scrollX: 0,
50
+ scrollY: 0,
51
+ },
52
+ finalScale: 1,
53
+ getPosFromMouseEvent: vi.fn().mockReturnValue({ x: 50, y: 50 }),
54
+ updateConfig: vi.fn(),
55
+ };
56
+ playgroundGesture = new PlaygroundGesture(el, config);
57
+ });
58
+
59
+ describe('PlaygroundGesture handlePinch', () => {
60
+ it('should not update config when newScale is NaN', () => {
61
+ playgroundGesture.handlePinch({
62
+ first: true,
63
+ last: false,
64
+ originX: 100,
65
+ originY: 100,
66
+ newScale: NaN,
67
+ });
68
+
69
+ expect(config.updateConfig).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('should set _pinching to true on first pinch', () => {
73
+ playgroundGesture.handlePinch({
74
+ first: true,
75
+ last: false,
76
+ originX: 100,
77
+ originY: 100,
78
+ newScale: 1.5,
79
+ });
80
+
81
+ expect(playgroundGesture.pinching).toBe(true);
82
+ });
83
+
84
+ it('should set _pinching to false on last pinch', () => {
85
+ playgroundGesture.handlePinch({
86
+ first: false,
87
+ last: true,
88
+ originX: 100,
89
+ originY: 100,
90
+ newScale: 1.5,
91
+ });
92
+
93
+ expect(playgroundGesture.pinching).toBe(false);
94
+ });
95
+
96
+ it('should call updateConfig with correct parameters', () => {
97
+ playgroundGesture.handlePinch({
98
+ first: false,
99
+ last: false,
100
+ originX: 100,
101
+ originY: 100,
102
+ newScale: 2,
103
+ });
104
+
105
+ expect(config.updateConfig).toHaveBeenCalledWith(
106
+ expect.objectContaining({
107
+ scrollX: expect.any(Number),
108
+ scrollY: expect.any(Number),
109
+ zoom: 2,
110
+ })
111
+ );
112
+ });
113
+
114
+ it('should calculate correct scroll values', () => {
115
+ config.getPosFromMouseEvent.mockReturnValue({ x: 100, y: 100 });
116
+ config.finalScale = 1;
117
+ config.config.scrollX = 0;
118
+ config.config.scrollY = 0;
119
+
120
+ playgroundGesture.handlePinch({
121
+ first: false,
122
+ last: false,
123
+ originX: 100,
124
+ originY: 100,
125
+ newScale: 2,
126
+ });
127
+
128
+ expect(config.updateConfig).toHaveBeenCalledWith({
129
+ scrollX: 100,
130
+ scrollY: 100,
131
+ zoom: 2,
132
+ });
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { Disposable, DisposableImpl } from '@flowgram-vue/utils';
7
+
8
+ import { type PlaygroundConfigEntity } from '../layer/config/playground-config-entity';
9
+ import { Gesture } from './use-gesture';
10
+
11
+ /* istanbul ignore next */
12
+ export class PlaygroundGesture extends DisposableImpl {
13
+ private _pinching = false;
14
+
15
+ constructor(
16
+ public readonly target: HTMLElement,
17
+ protected readonly config: PlaygroundConfigEntity
18
+ ) {
19
+ super();
20
+ this.preventDefault();
21
+ const gesture = new Gesture(
22
+ target,
23
+ {
24
+ // onDrag: ({pinching, cancel, offset: [x, y], ...rest}) => {
25
+ // if (pinching) return cancel();
26
+ // onChange({ ...style, x, y })
27
+ // api.start({ x, y })
28
+ // },
29
+ onPinch: ({
30
+ origin: [originX, originY],
31
+ first,
32
+ last,
33
+ movement: [ms],
34
+ offset: [newScale, a],
35
+ }) => {
36
+ this.handlePinch({ first, last, originX, originY, newScale });
37
+ },
38
+ },
39
+ {
40
+ // drag: { from: () => [startState.x, startState.y] },
41
+ pinch: {
42
+ scaleBounds: () => this.getScaleBounds(),
43
+ from: () => [this.config.finalScale, 0],
44
+ /**
45
+ * 支持 command 和 ctrl
46
+ */
47
+ modifierKey: ['metaKey', 'ctrlKey'],
48
+ // rubberband: true
49
+ },
50
+ }
51
+ );
52
+ this.toDispose.push(
53
+ Disposable.create(() => {
54
+ gesture.destroy();
55
+ })
56
+ );
57
+ }
58
+
59
+ handlePinch(params: {
60
+ first: boolean;
61
+ last: boolean;
62
+ originX: number;
63
+ originY: number;
64
+ newScale: number;
65
+ }) {
66
+ const { first, last, originX, originY, newScale } = params;
67
+ if (Number.isNaN(params.newScale)) {
68
+ // 防止画布无法缩放
69
+ return;
70
+ }
71
+ if (first) {
72
+ this._pinching = true;
73
+ }
74
+ if (last) {
75
+ this._pinching = false;
76
+ }
77
+ const oldScale = this.config.finalScale;
78
+ const origin = this.config.getPosFromMouseEvent({ clientX: originX, clientY: originY }, false);
79
+ // 放大后的位置
80
+ const finalPos = {
81
+ x: (origin.x / oldScale) * newScale,
82
+ y: (origin.y / oldScale) * newScale,
83
+ };
84
+ this.config.updateConfig({
85
+ scrollX: this.config.config.scrollX + finalPos.x - origin.x,
86
+ scrollY: this.config.config.scrollY + finalPos.y - origin.y,
87
+ zoom: newScale,
88
+ });
89
+ }
90
+
91
+ getScaleBounds(): { min: number; max: number } {
92
+ return {
93
+ min: this.config.config.minZoom,
94
+ max: this.config.config.maxZoom,
95
+ };
96
+ }
97
+
98
+ protected preventDefault(): void {
99
+ // 阻止默认手势
100
+ const handler = (e: MouseEvent) => e.preventDefault();
101
+ // @ts-ignore
102
+ document.addEventListener('gesturestart', handler);
103
+ // @ts-ignore
104
+ document.addEventListener('gesturechange', handler);
105
+ this.toDispose.push(
106
+ Disposable.create(() => {
107
+ // @ts-ignore
108
+ document.removeEventListener('gesturestart', handler);
109
+ // @ts-ignore
110
+ document.removeEventListener('gesturechange', handler);
111
+ })
112
+ );
113
+ }
114
+
115
+ get pinching(): boolean {
116
+ return this._pinching;
117
+ }
118
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import TWEEN from '@tweenjs/tween.js';
7
+ import { Disposable } from '@flowgram-vue/utils';
8
+
9
+ let started = 0;
10
+
11
+ // Setup the animation loop.
12
+ function startTweenLoop(): void {
13
+ started++;
14
+ function animate(time: number): void {
15
+ if (started <= 0) return;
16
+ requestAnimationFrame(animate);
17
+ TWEEN.update(time);
18
+ }
19
+ requestAnimationFrame(animate);
20
+ }
21
+
22
+ function stopTweenLoop(): void {
23
+ started--;
24
+ }
25
+
26
+ interface TweenValues {
27
+ [key: string]: number;
28
+ }
29
+
30
+ export interface TweenOpts<V> {
31
+ from: V;
32
+ to: V;
33
+ onUpdate?: (v: V) => void;
34
+ onComplete?: (v: V) => void;
35
+ onDispose?: (v: V) => void;
36
+ easing?: (num: number) => number;
37
+ duration: number;
38
+ }
39
+
40
+ export function startTween<V extends TweenValues = TweenValues>(opts: TweenOpts<V>): Disposable {
41
+ startTweenLoop();
42
+ let stopped = false;
43
+ const tween = new TWEEN.Tween(opts.from)
44
+ .to(opts.to, opts.duration)
45
+ .easing(opts.easing || TWEEN.Easing.Quadratic.Out)
46
+ .onUpdate(() => {
47
+ if (stopped) return;
48
+ if (opts.onUpdate) opts.onUpdate(opts.from);
49
+ })
50
+ .onComplete(() => {
51
+ if (stopped) return;
52
+ stopped = true;
53
+ stopTweenLoop();
54
+ if (opts.onComplete) opts.onComplete(opts.from);
55
+ })
56
+ .start();
57
+ return Disposable.create(() => {
58
+ if (stopped) return;
59
+ stopped = true;
60
+ stopTweenLoop();
61
+ tween.stop();
62
+ if (opts.onDispose) opts.onDispose(opts.from);
63
+ });
64
+ }
65
+
66
+ export interface ScrollIntoViewOpts {
67
+ getScrollParent(): HTMLElement | undefined;
68
+
69
+ getTargetNode(): HTMLElement | undefined;
70
+
71
+ duration?: number;
72
+ scrollY?: boolean; // 默认 true
73
+ scrollX?: boolean; // 默认 true
74
+ }
75
+
76
+ const defaultScrollIntoViewOpts = {
77
+ duration: 300,
78
+ scrollY: true,
79
+ scrollX: true,
80
+ };
81
+
82
+ const preTweenMap = new WeakMap<HTMLElement, Disposable>();
83
+
84
+ /**
85
+ * 滚动到可视区域
86
+ * @param opts
87
+ */
88
+ export function scrollIntoViewWithTween(opts: ScrollIntoViewOpts): Disposable {
89
+ opts = { ...defaultScrollIntoViewOpts, ...opts };
90
+ const parentNode = opts.getScrollParent();
91
+ const targetNode = opts.getTargetNode();
92
+ if (!parentNode || !targetNode) return Disposable.NULL;
93
+ // 销毁上一次的
94
+ if (preTweenMap.has(parentNode)) {
95
+ preTweenMap.get(parentNode)!.dispose();
96
+ }
97
+ const startScrollTop = parentNode.scrollTop;
98
+ const startScrollLeft = parentNode.scrollLeft;
99
+ let endScrollTop = startScrollTop;
100
+ let endScrollLeft = startScrollLeft;
101
+ const parentBound = parentNode.getBoundingClientRect();
102
+ const targetBound = targetNode.getBoundingClientRect();
103
+ const top = targetBound.top - parentBound.top + startScrollTop;
104
+ const left = targetBound.left - parentBound.left + startScrollLeft;
105
+ if (startScrollTop > top) {
106
+ endScrollTop = top;
107
+ } else {
108
+ const bottom = top + targetNode.clientHeight - parentNode.clientHeight;
109
+ if (startScrollTop < bottom && targetNode.clientHeight < parentNode.clientHeight) {
110
+ endScrollTop = bottom;
111
+ }
112
+ }
113
+ if (startScrollLeft > left) {
114
+ endScrollLeft = left;
115
+ } else {
116
+ const right = left + targetNode.clientWidth - parentNode.clientWidth;
117
+ if (startScrollLeft < right && targetNode.clientWidth < parentNode.clientWidth) {
118
+ endScrollLeft = right;
119
+ }
120
+ }
121
+ if (startScrollTop !== endScrollTop || startScrollLeft !== endScrollLeft) {
122
+ const from: { scrollTop?: number; scrollLeft?: number } = {};
123
+ const to: { scrollTop?: number; scrollLeft?: number } = {};
124
+ if (opts.scrollY) {
125
+ from.scrollTop = startScrollTop;
126
+ to.scrollTop = endScrollTop;
127
+ }
128
+ if (opts.scrollX) {
129
+ from.scrollLeft = startScrollLeft;
130
+ to.scrollLeft = endScrollLeft;
131
+ }
132
+ const scrollTween = startTween<{ scrollTop?: number; scrollLeft?: number }>({
133
+ from,
134
+ to,
135
+ onUpdate: v => {
136
+ if (v.scrollTop !== undefined) {
137
+ parentNode.scrollTop = v.scrollTop;
138
+ }
139
+ if (v.scrollLeft !== undefined) {
140
+ parentNode.scrollLeft = v.scrollLeft;
141
+ }
142
+ },
143
+ onComplete: () => {
144
+ toDispose.dispose();
145
+ },
146
+ duration: opts.duration!,
147
+ });
148
+ const toDispose = Disposable.create(() => {
149
+ scrollTween.dispose();
150
+ preTweenMap.delete(parentNode);
151
+ });
152
+ preTweenMap.set(parentNode, toDispose);
153
+ return toDispose;
154
+ }
155
+ return Disposable.NULL;
156
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { chain } from './utils/fn';
7
+ import { isTouch, parseProp, toHandlerProp, touchIds } from './utils/events';
8
+ import {
9
+ GestureKey,
10
+ InternalConfig,
11
+ InternalHandlers,
12
+ NativeHandlers,
13
+ State,
14
+ UserGestureConfig,
15
+ } from './types';
16
+ import { TimeoutStore } from './TimeoutStore';
17
+ import { EventStore } from './EventStore';
18
+ import { parse } from './config/resolver';
19
+ import { EngineMap } from './actions';
20
+
21
+ export class Controller {
22
+ /**
23
+ * The list of gestures handled by the Controller.
24
+ */
25
+ public gestures = new Set<GestureKey>();
26
+
27
+ /**
28
+ * The event store that keeps track of the config.target listeners.
29
+ */
30
+ private _targetEventStore = new EventStore(this);
31
+
32
+ /**
33
+ * Object that keeps track of all gesture event listeners.
34
+ */
35
+ public gestureEventStores: { [key in GestureKey]?: EventStore } = {};
36
+
37
+ public gestureTimeoutStores: { [key in GestureKey]?: TimeoutStore } = {};
38
+
39
+ public handlers: InternalHandlers = {};
40
+
41
+ private nativeHandlers?: NativeHandlers;
42
+
43
+ public config = {} as InternalConfig;
44
+
45
+ public pointerIds = new Set<number>();
46
+
47
+ public touchIds = new Set<number>();
48
+
49
+ public state = {
50
+ shared: {
51
+ shiftKey: false,
52
+ metaKey: false,
53
+ ctrlKey: false,
54
+ altKey: false,
55
+ },
56
+ } as State;
57
+
58
+ constructor(handlers: InternalHandlers) {
59
+ resolveGestures(this, handlers);
60
+ }
61
+
62
+ /**
63
+ * Sets pointer or touch ids based on the event.
64
+ * @param event
65
+ */
66
+ setEventIds(event: TouchEvent | PointerEvent) {
67
+ if (isTouch(event)) {
68
+ this.touchIds = new Set(touchIds(event as TouchEvent));
69
+ return this.touchIds;
70
+ } else if ('pointerId' in event) {
71
+ if (event.type === 'pointerup' || event.type === 'pointercancel')
72
+ this.pointerIds.delete(event.pointerId);
73
+ else if (event.type === 'pointerdown') this.pointerIds.add(event.pointerId);
74
+ return this.pointerIds;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Attaches handlers to the controller.
80
+ * @param handlers
81
+ * @param nativeHandlers
82
+ */
83
+ applyHandlers(handlers: InternalHandlers, nativeHandlers?: NativeHandlers) {
84
+ this.handlers = handlers;
85
+ this.nativeHandlers = nativeHandlers;
86
+ }
87
+
88
+ /**
89
+ * Compute and attaches a config to the controller.
90
+ * @param config
91
+ * @param gestureKey
92
+ */
93
+ applyConfig(config: UserGestureConfig, gestureKey?: GestureKey) {
94
+ this.config = parse(config, gestureKey, this.config);
95
+ }
96
+
97
+ /**
98
+ * Cleans all side effects (listeners, timeouts). When the gesture is
99
+ * destroyed (in React, when the component is unmounted.)
100
+ */
101
+ clean() {
102
+ this._targetEventStore.clean();
103
+ for (const key of this.gestures) {
104
+ this.gestureEventStores[key]!.clean();
105
+ this.gestureTimeoutStores[key]!.clean();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Executes side effects (attaching listeners to a `config.target`). Ran on
111
+ * each render.
112
+ */
113
+ effect() {
114
+ if (this.config.shared.target) this.bind();
115
+ return () => this._targetEventStore.clean();
116
+ }
117
+
118
+ /**
119
+ * The bind function that can be returned by the gesture handler (a hook in
120
+ * React for example.)
121
+ * @param args
122
+ */
123
+ bind(...args: any[]) {
124
+ const sharedConfig = this.config.shared;
125
+ const props: any = {};
126
+
127
+ let target;
128
+ if (sharedConfig.target) {
129
+ target = sharedConfig.target();
130
+ // if target is undefined let's stop
131
+ if (!target) return;
132
+ }
133
+
134
+ if (sharedConfig.enabled) {
135
+ // Adding gesture handlers
136
+ for (const gestureKey of this.gestures) {
137
+ const gestureConfig = this.config[gestureKey]!;
138
+ const bindFunction = bindToProps(props, gestureConfig.eventOptions, !!target);
139
+ if (gestureConfig.enabled) {
140
+ const Engine = EngineMap.get(gestureKey)!;
141
+ // @ts-ignore
142
+ new Engine(this, args, gestureKey).bind(bindFunction);
143
+ }
144
+ }
145
+
146
+ // Adding native handlers
147
+ const nativeBindFunction = bindToProps(props, sharedConfig.eventOptions, !!target);
148
+ for (const eventKey in this.nativeHandlers) {
149
+ nativeBindFunction(
150
+ eventKey,
151
+ '',
152
+ // @ts-ignore
153
+ event => this.nativeHandlers[eventKey]({ ...this.state.shared, event, args }),
154
+ undefined,
155
+ true,
156
+ );
157
+ }
158
+ }
159
+
160
+ // If target isn't set, we return an object that contains gesture handlers
161
+ // mapped to props handler event keys.
162
+ for (const handlerProp in props) {
163
+ props[handlerProp] = chain(...props[handlerProp]);
164
+ }
165
+
166
+ // When target isn't specified then return hanlder props.
167
+ if (!target) return props;
168
+
169
+ // When target is specified, then add listeners to the controller target
170
+ // store.
171
+ for (const handlerProp in props) {
172
+ const { device, capture, passive } = parseProp(handlerProp);
173
+ this._targetEventStore.add(target, device, '', props[handlerProp], { capture, passive });
174
+ }
175
+ }
176
+ }
177
+
178
+ function setupGesture(ctrl: Controller, gestureKey: GestureKey) {
179
+ ctrl.gestures.add(gestureKey);
180
+ ctrl.gestureEventStores[gestureKey] = new EventStore(ctrl, gestureKey);
181
+ ctrl.gestureTimeoutStores[gestureKey] = new TimeoutStore();
182
+ }
183
+
184
+ function resolveGestures(ctrl: Controller, internalHandlers: InternalHandlers) {
185
+ // make sure hover handlers are added first to prevent bugs such as #322
186
+ // where the hover pointerLeave handler is removed before the move
187
+ // pointerLeave, which prevents hovering: false to be fired.
188
+ if (internalHandlers.drag) setupGesture(ctrl, 'drag');
189
+ if (internalHandlers.wheel) setupGesture(ctrl, 'wheel');
190
+ if (internalHandlers.scroll) setupGesture(ctrl, 'scroll');
191
+ if (internalHandlers.move) setupGesture(ctrl, 'move');
192
+ if (internalHandlers.pinch) setupGesture(ctrl, 'pinch');
193
+ if (internalHandlers.hover) setupGesture(ctrl, 'hover');
194
+ }
195
+
196
+ const bindToProps =
197
+ (props: any, eventOptions: AddEventListenerOptions, withPassiveOption: boolean) =>
198
+ (
199
+ device: string,
200
+ action: string,
201
+ handler: (event: any) => void,
202
+ options: AddEventListenerOptions = {},
203
+ isNative = false,
204
+ ) => {
205
+ const capture = options.capture ?? eventOptions.capture;
206
+ const passive = options.passive ?? eventOptions.passive;
207
+ // a native handler is already passed as a prop like "onMouseDown"
208
+ let handlerProp = isNative ? device : toHandlerProp(device, action, capture);
209
+ if (withPassiveOption && passive) handlerProp += 'Passive';
210
+ props[handlerProp] = props[handlerProp] || [];
211
+ props[handlerProp].push(handler);
212
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { toDomEventType } from './utils/events';
7
+ import { GestureKey } from './types';
8
+ import type { Controller } from './Controller';
9
+
10
+ export class EventStore {
11
+ private _listeners = new Set<() => void>();
12
+
13
+ private _ctrl: Controller;
14
+
15
+ private _gestureKey?: GestureKey;
16
+
17
+ constructor(ctrl: Controller, gestureKey?: GestureKey) {
18
+ this._ctrl = ctrl;
19
+ this._gestureKey = gestureKey;
20
+ }
21
+
22
+ add(
23
+ element: EventTarget,
24
+ device: string,
25
+ action: string,
26
+ handler: (event: any) => void,
27
+ options?: AddEventListenerOptions,
28
+ ) {
29
+ const listeners = this._listeners;
30
+ const type = toDomEventType(device, action);
31
+ const _options = this._gestureKey ? this._ctrl.config[this._gestureKey]!.eventOptions : {};
32
+ const eventOptions = { ..._options, ...options };
33
+ element.addEventListener(type, handler, eventOptions);
34
+ const remove = () => {
35
+ element.removeEventListener(type, handler, eventOptions);
36
+ listeners.delete(remove);
37
+ };
38
+ listeners.add(remove);
39
+ return remove;
40
+ }
41
+
42
+ clean() {
43
+ this._listeners.forEach(remove => remove());
44
+ this._listeners.clear(); // just for safety
45
+ }
46
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export class TimeoutStore {
7
+ private _timeouts = new Map<string, number>();
8
+
9
+ add<FunctionType extends (...args: any[]) => any>(
10
+ key: string,
11
+ callback: FunctionType,
12
+ ms = 140,
13
+ ...args: Parameters<FunctionType>
14
+ ) {
15
+ this.remove(key);
16
+ this._timeouts.set(key, window.setTimeout(callback, ms, ...args));
17
+ }
18
+
19
+ remove(key: string) {
20
+ const timeout = this._timeouts.get(key);
21
+ if (timeout) window.clearTimeout(timeout);
22
+ }
23
+
24
+ clean() {
25
+ this._timeouts.forEach(timeout => void window.clearTimeout(timeout));
26
+ this._timeouts.clear();
27
+ }
28
+ }