diff --git a/packages/g6/__tests__/demos/behavior-scroll-canvas.ts b/packages/g6/__tests__/demos/behavior-scroll-canvas.ts new file mode 100644 index 00000000000..0a321d170d3 --- /dev/null +++ b/packages/g6/__tests__/demos/behavior-scroll-canvas.ts @@ -0,0 +1,35 @@ +import { Graph } from '@/src'; +import data from '@@/dataset/cluster.json'; + +export const behaviorScrollCanvas: TestCase = async (context) => { + const graph = new Graph({ + ...context, + data, + layout: { + type: 'd3force', + }, + node: { + style: { + size: 20, + }, + }, + behaviors: [ + { + type: 'scroll-canvas', + // direction: 'x', + // direction: 'y', + // sensitivity: 5, + // trigger: { + // up: ['ArrowUp'], + // down: ['ArrowDown'], + // right: ['ArrowRight'], + // left: ['ArrowLeft'], + // }, + }, + ], + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index a16e14c2ae5..3c77bdb0e80 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -13,6 +13,7 @@ export * from './behavior-drag-element'; export * from './behavior-focus-element'; export * from './behavior-hover-element'; export * from './behavior-lasso-select'; +export * from './behavior-scroll-canvas'; export * from './behavior-zoom-canvas'; export * from './combo'; export * from './combo-expand-collapse'; diff --git a/packages/g6/__tests__/snapshots/behaviors/behavior-scroll-canvas/default.svg b/packages/g6/__tests__/snapshots/behaviors/behavior-scroll-canvas/default.svg new file mode 100644 index 00000000000..eac7be699a1 --- /dev/null +++ b/packages/g6/__tests__/snapshots/behaviors/behavior-scroll-canvas/default.svg @@ -0,0 +1,2443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/behaviors/scroll-canvas/default.svg b/packages/g6/__tests__/snapshots/behaviors/scroll-canvas/default.svg new file mode 100644 index 00000000000..15a75a4f98f --- /dev/null +++ b/packages/g6/__tests__/snapshots/behaviors/scroll-canvas/default.svg @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts b/packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts new file mode 100644 index 00000000000..834983d6da6 --- /dev/null +++ b/packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts @@ -0,0 +1,104 @@ +import { behaviorScrollCanvas } from '@/__tests__/demos'; +import type { Graph } from '@/src'; +import { CommonEvent } from '@/src'; +import { createDemoGraph } from '@@/utils'; +import { isObject } from '@antv/util'; +import { ScrollCanvasOptions } from '../../../src/behaviors'; + +describe('behavior scroll canvas', () => { + let graph: Graph; + + beforeAll(async () => { + graph = await createDemoGraph(behaviorScrollCanvas, { animation: false }); + }); + + function setBehavior(options?: ScrollCanvasOptions) { + graph.setBehaviors((behaviors) => + behaviors.map((behavior) => { + if (isObject(behavior) && behavior.type === 'scroll-canvas') { + return { ...behavior, ...options }; + } + return behavior; + }), + ); + } + + it('default status', () => { + expect(graph.getBehaviors()).toEqual([ + { + type: 'scroll-canvas', + }, + ]); + }); + + function emitWheelEvent(options?: { deltaX: number; deltaY: number }) { + const dom = graph.getCanvas().getContextService().getDomElement(); + dom?.dispatchEvent(new WheelEvent(CommonEvent.WHEEL, options)); + } + + it('scroll', async () => { + const [x, y] = graph.getPosition(); + emitWheelEvent({ deltaX: -10, deltaY: -10 }); + expect(graph.getPosition()).toBeCloseTo([x + 10, y + 10]); + + await expect(graph).toMatchSnapshot(__filename); + }); + + it('direction', async () => { + setBehavior({ direction: 'x' }); + const [x, y] = graph.getPosition(); + emitWheelEvent({ deltaX: -10, deltaY: -10 }); + expect(graph.getPosition()).toBeCloseTo([x + 10, y]); + + setBehavior({ direction: undefined }); + }); + + it('sensitivity', () => { + const sensitivity = 5; + setBehavior({ sensitivity }); + const [x, y] = graph.getPosition(); + const deltaX = -10, + deltaY = -10; + emitWheelEvent({ deltaX, deltaY }); + expect(graph.getPosition()).toBeCloseTo([x + Math.abs(deltaX * sensitivity), y + Math.abs(deltaY * sensitivity)]); + }); + + const shortcutScrollCanvasOptions: ScrollCanvasOptions = { + key: 'shortcut-scroll-canvas', + type: 'scroll-canvas', + trigger: { + up: ['ArrowUp'], + down: ['ArrowDown'], + right: ['ArrowRight'], + left: ['ArrowLeft'], + }, + }; + + it('custom trigger', () => { + graph.setBehaviors((behavior) => [...behavior, shortcutScrollCanvasOptions]); + + let [x, y] = graph.getPosition(); + graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowUp' }); + graph.emit(CommonEvent.KEY_UP, { key: 'ArrowUp' }); + expect(graph.getPosition()).toBeCloseTo([x, y - 10]); + + [x, y] = graph.getPosition(); + graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' }); + graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' }); + expect(graph.getPosition()).toBeCloseTo([x, y + 10]); + + [x, y] = graph.getPosition(); + graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowLeft' }); + graph.emit(CommonEvent.KEY_UP, { key: 'ArrowLeft' }); + expect(graph.getPosition()).toBeCloseTo([x - 10, y]); + + [x, y] = graph.getPosition(); + graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' }); + graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' }); + expect(graph.getPosition()).toBeCloseTo([x + 10, y]); + }); + + it('destroy', () => { + graph.destroy(); + }); +}); diff --git a/packages/g6/src/behaviors/index.ts b/packages/g6/src/behaviors/index.ts index 370ef0c831a..4386011b9ba 100644 --- a/packages/g6/src/behaviors/index.ts +++ b/packages/g6/src/behaviors/index.ts @@ -9,6 +9,7 @@ export { DragElementForce } from './drag-element-force'; export { FocusElement } from './focus-element'; export { HoverElement } from './hover-element'; export { LassoSelect } from './lasso-select'; +export { ScrollCanvas } from './scroll-canvas'; export { ZoomCanvas } from './zoom-canvas'; export type { BaseBehaviorOptions } from './base-behavior'; @@ -22,4 +23,5 @@ export type { DragElementForceOptions } from './drag-element-force'; export type { FocusElementOptions } from './focus-element'; export type { HoverElementOptions } from './hover-element'; export type { LassoSelectOptions } from './lasso-select'; +export type { ScrollCanvasOptions } from './scroll-canvas'; export type { ZoomCanvasOptions } from './zoom-canvas'; diff --git a/packages/g6/src/behaviors/scroll-canvas.ts b/packages/g6/src/behaviors/scroll-canvas.ts new file mode 100644 index 00000000000..5493c6c599c --- /dev/null +++ b/packages/g6/src/behaviors/scroll-canvas.ts @@ -0,0 +1,141 @@ +import { isFunction, isObject } from '@antv/util'; +import { CanvasEvent } from '../constants'; +import type { RuntimeContext } from '../runtime/types'; +import type { IKeyboardEvent, Point } from '../types'; +import { Shortcut, ShortcutKey } from '../utils/shortcut'; +import type { BaseBehaviorOptions } from './base-behavior'; +import { BaseBehavior } from './base-behavior'; + +export interface ScrollCanvasOptions extends BaseBehaviorOptions { + /** + * 是否启用滚动画布的功能 + * + * Whether to enable the function of scrolling the canvas + */ + enable?: boolean | ((event: WheelEvent | IKeyboardEvent) => boolean); + /** + * 触发滚动的方式,默认使用指针滚动 + * + * The way to trigger scrolling, default to scrolling with the pointer pressed + */ + trigger?: CombinationKey; + /** + * 允许的滚动方向。选项有:"x"、"y",默认情况下没有限制 + * + * The allowed rolling direction. The options are "x" and "y", with no restrictions by default + */ + direction?: 'x' | 'y'; + /** + * 滚动灵敏度 + * + * Scroll sensitivity + */ + sensitivity?: number; + /** + * 完成滚动时的回调 + * + * Callback when scrolling is completed + */ + onfinish?: () => void; +} + +type CombinationKey = { + up: ShortcutKey; + down: ShortcutKey; + left: ShortcutKey; + right: ShortcutKey; +}; + +type EnableOptions = { + node?: boolean; + edge?: boolean; + combo?: boolean; +}; + +export class ScrollCanvas extends BaseBehavior { + static defaultOptions: ScrollCanvasOptions = { + enable: true, + sensitivity: 1, + }; + + private shortcut: Shortcut; + + constructor(context: RuntimeContext, options: ScrollCanvasOptions) { + super(context, Object.assign({}, ScrollCanvas.defaultOptions, options)); + + this.shortcut = new Shortcut(context.graph); + + this.bindEvents(); + } + + private bindEvents() { + const { trigger } = this.options; + this.shortcut.unbindAll(); + const { graph } = this.context; + if (isObject(trigger)) { + graph.off(CanvasEvent.WHEEL, this.onWheel); + const { up = [], down = [], left = [], right = [] } = trigger; + + this.shortcut.bind(up, (event) => this.scroll([0, -10], event)); + this.shortcut.bind(down, (event) => this.scroll([0, 10], event)); + this.shortcut.bind(left, (event) => this.scroll([-10, 0], event)); + this.shortcut.bind(right, (event) => this.scroll([10, 0], event)); + } else { + /** + * 这里必需在原生canvas上绑定wheel事件,参考: + * https://g.antv.antgroup.com/api/event/faq#%E5%9C%A8-chrome-%E4%B8%AD%E7%A6%81%E6%AD%A2%E9%A1%B5%E9%9D%A2%E9%BB%98%E8%AE%A4%E6%BB%9A%E5%8A%A8%E8%A1%8C%E4%B8%BA + */ + this.graphDom?.addEventListener(CanvasEvent.WHEEL, this.onWheel, { passive: false }); + } + } + + get graphDom() { + return this.context.graph.getCanvas().getContextService().getDomElement(); + } + + private onWheel = async (event: WheelEvent) => { + event.preventDefault(); + const diffX = event.deltaX; + const diffY = event.deltaY; + + await this.scroll([-diffX, -diffY], event); + }; + + private formatDisplacement([dx, dy]: Point) { + const { direction, sensitivity } = this.options; + + dx = dx * sensitivity; + dy = dy * sensitivity; + + if (direction === 'x') { + dy = 0; + } else if (direction === 'y') { + dx = 0; + } + + return [dx, dy] as Point; + } + + private async scroll(value: Point, event: WheelEvent | IKeyboardEvent) { + if (!this.validate(event)) return; + const { onfinish } = this.options; + const graph = this.context.graph; + const formattedValue = this.formatDisplacement(value); + await graph.translateBy(formattedValue, false); + onfinish?.(); + } + + private validate(event: WheelEvent | IKeyboardEvent) { + if (this.destroyed) return false; + + const { enable } = this.options; + if (isFunction(enable)) return enable(event); + return !!enable; + } + + public destroy(): void { + this.shortcut.destroy(); + this.graphDom?.removeEventListener(CanvasEvent.WHEEL, this.onWheel); + super.destroy(); + } +} diff --git a/packages/g6/src/behaviors/types.ts b/packages/g6/src/behaviors/types.ts index b7927848956..3133d13d00b 100644 --- a/packages/g6/src/behaviors/types.ts +++ b/packages/g6/src/behaviors/types.ts @@ -1,8 +1,9 @@ import type { BaseBehavior } from './base-behavior'; import type { DragCanvasOptions } from './drag-canvas'; +import type { ScrollCanvasOptions } from './scroll-canvas'; import type { ZoomCanvasOptions } from './zoom-canvas'; export type { States } from './brush-select'; -export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions; +export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions | ScrollCanvasOptions; export type Behavior = BaseBehavior; diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts index b8c1d3f96d2..a54000dc720 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -10,6 +10,7 @@ import { FocusElement, HoverElement, LassoSelect, + ScrollCanvas, ZoomCanvas, } from '../behaviors'; import { @@ -73,6 +74,7 @@ export const BUILT_IN_EXTENSIONS: ExtensionRegistry = { 'drag-canvas': DragCanvas, 'drag-element': DragElement, 'drag-element-force': DragElementForce, + 'scroll-canvas': ScrollCanvas, 'collapse-expand': CollapseExpand, 'click-element': ClickElement, 'hover-element': HoverElement,