@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.
- package/LICENSE +22 -0
- package/dist/index.cjs +0 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/common/config-entity.ts +80 -0
- package/src/common/entity-data.ts +171 -0
- package/src/common/entity-manager-contribution.ts +12 -0
- package/src/common/entity-manager.ts +475 -0
- package/src/common/entity.ts +482 -0
- package/src/common/index.ts +22 -0
- package/src/common/playground-context.ts +35 -0
- package/src/common/playground-decorator-helper.ts +113 -0
- package/src/common/playground-decorators.ts +85 -0
- package/src/common/playground-schedule.ts +30 -0
- package/src/common/protect-wheel-area.ts +11 -0
- package/src/common/schema/index.ts +14 -0
- package/src/common/schema/node.ts +15 -0
- package/src/common/schema/opacity-schema.ts +19 -0
- package/src/common/schema/origin-schema.ts +35 -0
- package/src/common/schema/position-schema.ts +35 -0
- package/src/common/schema/rotation-schema.ts +19 -0
- package/src/common/schema/scale-schema.ts +35 -0
- package/src/common/schema/size-schema.ts +42 -0
- package/src/common/schema/skew-schema.ts +35 -0
- package/src/common/schema/transform-schema.ts +613 -0
- package/src/common/utils/bounds.spec.ts +229 -0
- package/src/common/utils/bounds.ts +176 -0
- package/src/common/utils/index.ts +6 -0
- package/src/composables/index.ts +16 -0
- package/src/composables/use-config-entity.ts +31 -0
- package/src/composables/use-entities.ts +26 -0
- package/src/composables/use-entity-data-from-context.ts +34 -0
- package/src/composables/use-entity-from-context.ts +31 -0
- package/src/composables/use-listen-events.ts +22 -0
- package/src/composables/use-playground-container.ts +16 -0
- package/src/composables/use-playground-context.ts +15 -0
- package/src/composables/use-playground-drag.ts +31 -0
- package/src/composables/use-playground.ts +16 -0
- package/src/composables/use-refresh.ts +6 -0
- package/src/composables/use-service.ts +17 -0
- package/src/core/index.ts +8 -0
- package/src/core/layer/config/editor-state-config-entity.ts +180 -0
- package/src/core/layer/config/index.ts +7 -0
- package/src/core/layer/config/playground-config-entity.ts +604 -0
- package/src/core/layer/index.ts +8 -0
- package/src/core/layer/layer.ts +248 -0
- package/src/core/layer/playground-layer.ts +547 -0
- package/src/core/pipeline/index.ts +10 -0
- package/src/core/pipeline/pipeline-entities-selector.ts +200 -0
- package/src/core/pipeline/pipeline-entities.ts +230 -0
- package/src/core/pipeline/pipeline-registry.ts +344 -0
- package/src/core/pipeline/pipeline-renderer.ts +248 -0
- package/src/core/pipeline/pipeline-vue-utils.ts +96 -0
- package/src/core/pipeline/pipeline.ts +28 -0
- package/src/core/utils/index.ts +12 -0
- package/src/core/utils/inject-provider-decorators.ts +27 -0
- package/src/core/utils/lazy-inject-decorators.ts +38 -0
- package/src/core/utils/mouse-touch-event.ts +120 -0
- package/src/core/utils/playground-drag.ts +511 -0
- package/src/core/utils/playground-gesture.spec.ts +135 -0
- package/src/core/utils/playground-gesture.ts +118 -0
- package/src/core/utils/tween.ts +156 -0
- package/src/core/utils/use-gesture/core/Controller.ts +212 -0
- package/src/core/utils/use-gesture/core/EventStore.ts +46 -0
- package/src/core/utils/use-gesture/core/TimeoutStore.ts +28 -0
- package/src/core/utils/use-gesture/core/actions.ts +63 -0
- package/src/core/utils/use-gesture/core/config/commonConfigResolver.ts +86 -0
- package/src/core/utils/use-gesture/core/config/coordinatesConfigResolver.ts +48 -0
- package/src/core/utils/use-gesture/core/config/dragConfigResolver.ts +140 -0
- package/src/core/utils/use-gesture/core/config/hoverConfigResolver.ts +11 -0
- package/src/core/utils/use-gesture/core/config/moveConfigResolver.ts +11 -0
- package/src/core/utils/use-gesture/core/config/pinchConfigResolver.ts +59 -0
- package/src/core/utils/use-gesture/core/config/resolver.ts +70 -0
- package/src/core/utils/use-gesture/core/config/scrollConfigResolver.ts +8 -0
- package/src/core/utils/use-gesture/core/config/sharedConfigResolver.ts +28 -0
- package/src/core/utils/use-gesture/core/config/support.ts +52 -0
- package/src/core/utils/use-gesture/core/config/wheelConfigResolver.ts +8 -0
- package/src/core/utils/use-gesture/core/engines/CoordinatesEngine.ts +78 -0
- package/src/core/utils/use-gesture/core/engines/DragEngine.ts +403 -0
- package/src/core/utils/use-gesture/core/engines/Engine.ts +406 -0
- package/src/core/utils/use-gesture/core/engines/HoverEngine.ts +43 -0
- package/src/core/utils/use-gesture/core/engines/MoveEngine.ts +52 -0
- package/src/core/utils/use-gesture/core/engines/PinchEngine.ts +345 -0
- package/src/core/utils/use-gesture/core/engines/ScrollEngine.ts +42 -0
- package/src/core/utils/use-gesture/core/engines/WheelEngine.ts +48 -0
- package/src/core/utils/use-gesture/core/index.ts +7 -0
- package/src/core/utils/use-gesture/core/parser.ts +92 -0
- package/src/core/utils/use-gesture/core/types/action.ts +19 -0
- package/src/core/utils/use-gesture/core/types/config.ts +255 -0
- package/src/core/utils/use-gesture/core/types/handlers.ts +73 -0
- package/src/core/utils/use-gesture/core/types/index.ts +11 -0
- package/src/core/utils/use-gesture/core/types/internalConfig.ts +80 -0
- package/src/core/utils/use-gesture/core/types/state.ts +275 -0
- package/src/core/utils/use-gesture/core/types/utils.ts +204 -0
- package/src/core/utils/use-gesture/core/types.ts +8 -0
- package/src/core/utils/use-gesture/core/utils/events.ts +158 -0
- package/src/core/utils/use-gesture/core/utils/fn.ts +32 -0
- package/src/core/utils/use-gesture/core/utils/maths.ts +63 -0
- package/src/core/utils/use-gesture/core/utils/state.ts +24 -0
- package/src/core/utils/use-gesture/core/utils.ts +8 -0
- package/src/core/utils/use-gesture/index.ts +6 -0
- package/src/core/utils/use-gesture/vanilla/DragGesture.ts +27 -0
- package/src/core/utils/use-gesture/vanilla/Gesture.ts +45 -0
- package/src/core/utils/use-gesture/vanilla/HoverGesture.ts +27 -0
- package/src/core/utils/use-gesture/vanilla/MoveGesture.ts +27 -0
- package/src/core/utils/use-gesture/vanilla/PinchGesture.ts +27 -0
- package/src/core/utils/use-gesture/vanilla/Recognizer.ts +43 -0
- package/src/core/utils/use-gesture/vanilla/ScrollGesture.ts +27 -0
- package/src/core/utils/use-gesture/vanilla/WheelGesture.ts +27 -0
- package/src/core/utils/use-gesture/vanilla/createGesture.ts +18 -0
- package/src/core/utils/use-gesture/vanilla/index.ts +18 -0
- package/src/env.d.ts +10 -0
- package/src/index.ts +17 -0
- package/src/playground-config.ts +70 -0
- package/src/playground-container.ts +129 -0
- package/src/playground-contribution.ts +81 -0
- package/src/playground-mock-tools.ts +143 -0
- package/src/playground.ts +401 -0
- package/src/plugin/index.ts +6 -0
- package/src/plugin/plugin.ts +226 -0
- package/src/services/clipboard-service.ts +40 -0
- package/src/services/context-menu-service.ts +25 -0
- package/src/services/index.ts +10 -0
- package/src/services/logger-service.ts +49 -0
- package/src/services/selection-service.ts +54 -0
- package/src/services/storage-service.ts +65 -0
- package/src/vue/index.ts +8 -0
- package/src/vue/playground-vue-context.ts +22 -0
- package/src/vue/playground-vue-provider.ts +115 -0
- 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
|
+
}
|