@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
430 lines (371 loc) • 10.9 kB
text/typescript
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.',
)
}
}
dispose() {
this.scrollerImpl.dispose()
this.stopListening()
this.off()
CssLoader.clean(this.name)
}
}