UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

430 lines (371 loc) 10.9 kB
import { Basecoat, CssLoader, Dom, disposable, isModifierKeyMatch, type ModifierKey, } from '../../common' import { Config } from '../../config' import type { Point, PointLike, Rectangle, RectangleLike } from '../../geometry' import type { BackgroundManagerOptions, GetContentAreaOptions, Graph, GraphPlugin, ScaleContentToFitOptions, ZoomOptions, } from '../../graph' import type { Cell } from '../../model' import type { CenterOptions, Direction, EventArgs, PositionContentOptions, Options as SOptions, TransitionOptions, TransitionToRectOptions, } from './scroller' import { getOptions, ScrollerImpl } from './scroller' import { content } from './style/raw' import './api' export interface ScrollerEventArgs extends EventArgs {} interface Options extends SOptions { pannable?: | boolean | { enabled: boolean eventTypes: Array<'leftMouseDown' | 'rightMouseDown'> } modifiers?: string | ModifierKey[] | null // alt, ctrl, shift, meta } export type ScrollerOptions = Omit<Options, 'graph'> export class Scroller extends Basecoat<ScrollerEventArgs> implements GraphPlugin { public name = 'scroller' public options: ScrollerOptions private graph: Graph private scrollerImpl: ScrollerImpl get pannable() { if (this.options) { if (typeof this.options.pannable === 'object') { return this.options.pannable.enabled } return !!this.options.pannable } return false } get container() { return this.scrollerImpl.container } constructor(options: ScrollerOptions = {}) { super() this.options = options CssLoader.ensure(this.name, content) } public init(graph: Graph) { this.graph = graph const options = getOptions({ enabled: true, ...this.options, graph, }) this.options = options this.scrollerImpl = new ScrollerImpl(options) this.setup() this.autoDisableGraphPanning() this.startListening() this.updateClassName() this.scrollerImpl.center() } // #region api resize(width?: number, height?: number) { this.scrollerImpl.resize(width, height) } resizePage(width?: number, height?: number) { this.scrollerImpl.updatePageSize(width, height) } zoom(): number zoom(factor: number, options?: ZoomOptions): this zoom(factor?: number, options?: ZoomOptions) { if (typeof factor === 'undefined') { return this.scrollerImpl.zoom() } this.scrollerImpl.zoom(factor, options) return this } zoomTo(factor: number, options: Omit<ZoomOptions, 'absolute'> = {}) { this.scrollerImpl.zoom(factor, { ...options, absolute: true }) return this } zoomToRect( rect: RectangleLike, options: ScaleContentToFitOptions & ScaleContentToFitOptions = {}, ) { this.scrollerImpl.zoomToRect(rect, options) return this } zoomToFit(options: GetContentAreaOptions & ScaleContentToFitOptions = {}) { this.scrollerImpl.zoomToFit(options) return this } center(optons?: CenterOptions) { return this.centerPoint(optons) } centerPoint(x: number, y: null | number, options?: CenterOptions): this centerPoint(x: null | number, y: number, options?: CenterOptions): this centerPoint(optons?: CenterOptions): this centerPoint( x?: number | null | CenterOptions, y?: number | null, options?: CenterOptions, ) { this.scrollerImpl.centerPoint(x as number, y as number, options) return this } centerContent(options?: PositionContentOptions) { this.scrollerImpl.centerContent(options) return this } centerCell(cell: Cell, options?: CenterOptions) { this.scrollerImpl.centerCell(cell, options) return this } positionPoint( point: PointLike, x: number | string, y: number | string, options: CenterOptions = {}, ) { this.scrollerImpl.positionPoint(point, x, y, options) return this } positionRect( rect: RectangleLike, direction: Direction, options?: CenterOptions, ) { this.scrollerImpl.positionRect(rect, direction, options) return this } positionCell(cell: Cell, direction: Direction, options?: CenterOptions) { this.scrollerImpl.positionCell(cell, direction, options) return this } positionContent(pos: Direction, options?: PositionContentOptions) { this.scrollerImpl.positionContent(pos, options) return this } drawBackground(options?: BackgroundManagerOptions, onGraph?: boolean) { if (this.graph.options.background == null || !onGraph) { this.scrollerImpl.backgroundManager.draw(options) } return this } clearBackground(onGraph?: boolean) { if (this.graph.options.background == null || !onGraph) { this.scrollerImpl.backgroundManager.clear() } return this } isPannable() { return this.pannable } enablePanning() { if (!this.pannable) { this.options.pannable = true this.updateClassName() } } disablePanning() { if (this.pannable) { this.options.pannable = false this.updateClassName() } } togglePanning(pannable?: boolean) { if (pannable == null) { if (this.isPannable()) { this.disablePanning() } else { this.enablePanning() } } else if (pannable !== this.isPannable()) { if (pannable) { this.enablePanning() } else { this.disablePanning() } } return this } lockScroller() { this.scrollerImpl.lock() return this } unlockScroller() { this.scrollerImpl.unlock() return this } updateScroller() { this.scrollerImpl.update() return this } getScrollbarPosition() { return this.scrollerImpl.scrollbarPosition() } setScrollbarPosition(left?: number, top?: number) { this.scrollerImpl.scrollbarPosition(left, top) return this } scrollToPoint(x: number | null | undefined, y: number | null | undefined) { this.scrollerImpl.scrollToPoint(x, y) return this } scrollToContent() { this.scrollerImpl.scrollToContent() return this } scrollToCell(cell: Cell) { this.scrollerImpl.scrollToCell(cell) return this } transitionToPoint(p: PointLike, options?: TransitionOptions): this transitionToPoint(x: number, y: number, options?: TransitionOptions): this transitionToPoint( x: number | PointLike, y?: number | TransitionOptions, options?: TransitionOptions, ) { this.scrollerImpl.transitionToPoint(x as number, y as number, options) return this } transitionToRect(rect: RectangleLike, options: TransitionToRectOptions = {}) { this.scrollerImpl.transitionToRect(rect, options) return this } enableAutoResize() { this.scrollerImpl.enableAutoResize() } disableAutoResize() { this.scrollerImpl.disableAutoResize() } autoScroll(clientX: number, clientY: number) { return this.scrollerImpl.autoScroll(clientX, clientY) } clientToLocalPoint(x: number, y: number): Point { return this.scrollerImpl.clientToLocalPoint(x, y) } getVisibleArea(): Rectangle { return this.scrollerImpl.getVisibleArea() } isCellVisible(cell: Cell, options: { strict?: boolean } = {}) { return this.scrollerImpl.isCellVisible(cell, options) } isPointVisible(point: PointLike) { return this.scrollerImpl.isPointVisible(point) } // #endregion protected setup() { this.scrollerImpl.on('*', (name, args) => { this.trigger(name, args) }) } protected startListening() { let eventTypes = [] const pannable = this.options.pannable if (typeof pannable === 'object') { eventTypes = pannable.eventTypes || [] } else { eventTypes = ['leftMouseDown'] } if (eventTypes.includes('leftMouseDown')) { this.graph.on('blank:mousedown', this.preparePanning, this) this.graph.on('node:unhandled:mousedown', this.preparePanning, this) this.graph.on('edge:unhandled:mousedown', this.preparePanning, this) } if (eventTypes.includes('rightMouseDown')) { this.onRightMouseDown = this.onRightMouseDown.bind(this) Dom.Event.on( this.scrollerImpl.container, 'mousedown', this.onRightMouseDown, ) } } protected stopListening() { let eventTypes = [] const pannable = this.options.pannable if (typeof pannable === 'object') { eventTypes = pannable.eventTypes || [] } else { eventTypes = ['leftMouseDown'] } if (eventTypes.includes('leftMouseDown')) { this.graph.off('blank:mousedown', this.preparePanning, this) this.graph.off('node:unhandled:mousedown', this.preparePanning, this) this.graph.off('edge:unhandled:mousedown', this.preparePanning, this) } if (eventTypes.includes('rightMouseDown')) { Dom.Event.off( this.scrollerImpl.container, 'mousedown', this.onRightMouseDown, ) } } protected onRightMouseDown(e: Dom.MouseDownEvent) { if (e.button === 2 && this.allowPanning(e, true)) { this.updateClassName(true) this.scrollerImpl.startPanning(e) this.scrollerImpl.once('pan:stop', () => this.updateClassName(false)) } } protected preparePanning({ e }: { e: Dom.MouseDownEvent }) { const allowPanning = this.allowPanning(e, true) const selection = this.graph.getPlugin<any>('selection') const allowRubberband = selection && selection.allowRubberband(e, true) if (allowPanning || (this.allowPanning(e) && !allowRubberband)) { this.updateClassName(true) this.scrollerImpl.startPanning(e) this.scrollerImpl.once('pan:stop', () => this.updateClassName(false)) } } protected allowPanning(e: Dom.MouseDownEvent, strict?: boolean) { return ( this.pannable && isModifierKeyMatch(e, this.options.modifiers, strict) ) } protected updateClassName(isPanning?: boolean) { const container = this.scrollerImpl.container! const pannable = Config.prefix('graph-scroller-pannable') if (this.pannable) { Dom.addClass(container, pannable) container.dataset.panning = (!!isPanning).toString() // Use dataset to control scroller panning style to avoid reflow caused by changing classList } else { Dom.removeClass(container, pannable) } } /** * 当 Scroller 插件启用时,默认关闭 Graph 的内置 panning, * 以避免滚动容器的拖拽与画布平移产生冲突。 */ protected autoDisableGraphPanning() { const graphPan = this.graph?.panning if (graphPan?.pannable) { graphPan.disablePanning() console.warn( 'Detected Scroller plugin; Graph panning has been disabled by default to avoid conflicts.', ) } } @disposable() dispose() { this.scrollerImpl.dispose() this.stopListening() this.off() CssLoader.clean(this.name) } }