@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
420 lines (364 loc) • 11.6 kB
text/typescript
import { type Dom, isModifierKeyMatch, type ModifierKey } from '../../common'
import { Config } from '../../config'
import { Point, type PointLike } from '../../geometry'
import type { Graph } from '../../graph'
import type { Edge } from '../../model/edge'
import type { EdgeView } from '../../view/edge'
import { ToolItem, type ToolItemOptions } from '../../view/tool'
import { View } from '../../view/view'
import { createViewElement } from '../../view/view/util'
import type { SimpleAttrs } from '../attr'
const pathClassName = Config.prefix('edge-tool-vertex-path')
export class Vertices extends ToolItem<EdgeView, Options> {
public static defaults: Options = {
...ToolItem.getDefaults(),
name: 'vertices',
snapRadius: 20,
addable: true,
removable: true,
removeRedundancies: true,
stopPropagation: true,
attrs: {
r: 6,
fill: '#333',
stroke: '#fff',
cursor: 'move',
'stroke-width': 2,
},
createHandle: (options) => new Handle(options),
markup: [
{
tagName: 'path',
selector: 'connection',
className: pathClassName,
attrs: {
fill: 'none',
stroke: 'transparent',
'stroke-width': 10,
cursor: 'pointer',
},
},
],
events: {
[`mousedown .${pathClassName}`]: 'onPathMouseDown',
[`touchstart .${pathClassName}`]: 'onPathMouseDown',
},
}
protected handles: Handle[] = []
protected get vertices() {
return this.cellView.cell.getVertices()
}
protected onRender() {
this.addClass(this.prefixClassName('edge-tool-vertices'))
if (this.options.addable) {
this.updatePath()
}
this.resetHandles()
this.renderHandles()
return this
}
update() {
const vertices = this.vertices
if (vertices.length === this.handles.length) {
this.updateHandles()
} else {
this.resetHandles()
this.renderHandles()
}
if (this.options.addable) {
this.updatePath()
}
return this
}
protected resetHandles() {
const handles = this.handles
this.handles = []
if (handles) {
handles.forEach((handle) => {
this.stopHandleListening(handle)
handle.remove()
})
}
}
protected renderHandles() {
const vertices = this.vertices
for (let i = 0, l = vertices.length; i < l; i += 1) {
const vertex = vertices[i]
const createHandle = this.options.createHandle!
const processHandle = this.options.processHandle
const handle = createHandle({
index: i,
graph: this.graph,
guard: (evt: Dom.EventObject) => this.guard(evt), // eslint-disable-line no-loop-func
attrs: this.options.attrs || {},
})
if (processHandle) {
processHandle(handle)
}
handle.updatePosition(vertex.x, vertex.y)
this.stamp(handle.container)
this.container.appendChild(handle.container)
this.handles.push(handle)
this.startHandleListening(handle)
}
}
protected updateHandles() {
const vertices = this.vertices
for (let i = 0, l = vertices.length; i < l; i += 1) {
const vertex = vertices[i]
const handle = this.handles[i]
if (handle) {
handle.updatePosition(vertex.x, vertex.y)
}
}
}
protected updatePath() {
const connection = this.childNodes.connection
if (connection) {
connection.setAttribute('d', this.cellView.getConnectionPathData())
}
}
protected startHandleListening(handle: Handle) {
const edgeView = this.cellView
if (edgeView.can('vertexMovable')) {
handle.on('change', this.onHandleChange, this)
handle.on('changing', this.onHandleChanging, this)
handle.on('changed', this.onHandleChanged, this)
}
if (edgeView.can('vertexDeletable')) {
handle.on('remove', this.onHandleRemove, this)
}
}
protected stopHandleListening(handle: Handle) {
const edgeView = this.cellView
if (edgeView.can('vertexMovable')) {
handle.off('change', this.onHandleChange, this)
handle.off('changing', this.onHandleChanging, this)
handle.off('changed', this.onHandleChanged, this)
}
if (edgeView.can('vertexDeletable')) {
handle.off('remove', this.onHandleRemove, this)
}
}
protected getNeighborPoints(index: number) {
const edgeView = this.cellView
const vertices = this.vertices
const prev = index > 0 ? vertices[index - 1] : edgeView.sourceAnchor
const next =
index < vertices.length - 1 ? vertices[index + 1] : edgeView.targetAnchor
return {
prev: Point.create(prev),
next: Point.create(next),
}
}
protected getMouseEventArgs<T extends Dom.EventObject>(evt: T) {
const e = this.normalizeEvent(evt)
const { x, y } = this.graph.snapToGrid(e.clientX!, e.clientY!)
return { e, x, y }
}
protected onHandleChange({ e }: EventArgs['change']) {
this.focus()
const edgeView = this.cellView
edgeView.cell.startBatch('move-vertex', { ui: true, toolId: this.cid })
if (!this.options.stopPropagation) {
const { e: evt, x, y } = this.getMouseEventArgs(e)
this.eventData(evt, { start: { x, y } })
edgeView.notifyMouseDown(evt, x, y)
}
}
protected onHandleChanging({ handle, e }: EventArgs['changing']) {
const edgeView = this.cellView
const index = handle.options.index
const { e: evt, x, y } = this.getMouseEventArgs(e)
const vertex = { x, y }
this.snapVertex(vertex, index)
edgeView.cell.setVertexAt(index, vertex, { ui: true, toolId: this.cid })
handle.updatePosition(vertex.x, vertex.y)
if (!this.options.stopPropagation) {
edgeView.notifyMouseMove(evt, x, y)
}
}
protected stopBatch(vertexAdded: boolean) {
this.cell.stopBatch('move-vertex', { ui: true, toolId: this.cid })
if (vertexAdded) {
this.cell.stopBatch('add-vertex', { ui: true, toolId: this.cid })
}
}
protected onHandleChanged({ e }: EventArgs['changed']) {
const options = this.options
const edgeView = this.cellView
if (options.addable) {
this.updatePath()
}
if (options.removeRedundancies) {
const verticesRemoved = edgeView.removeRedundantLinearVertices({
ui: true,
toolId: this.cid,
})
if (verticesRemoved) {
this.render()
}
}
this.blur()
this.stopBatch(this.eventData(e).vertexAdded)
const { e: evt, x, y } = this.getMouseEventArgs(e)
if (!this.options.stopPropagation) {
edgeView.notifyMouseUp(evt, x, y)
const { start } = this.eventData(evt)
if (start) {
const { x: startX, y: startY } = start
if (startX === x && startY === y) {
edgeView.onClick(evt as unknown as Dom.ClickEvent, x, y)
}
}
}
edgeView.checkMouseleave(evt)
options.onChanged && options.onChanged({ edge: edgeView.cell, edgeView })
}
protected snapVertex(vertex: PointLike, index: number) {
const snapRadius = this.options.snapRadius || 0
if (snapRadius > 0) {
const neighbors = this.getNeighborPoints(index)
const prev = neighbors.prev
const next = neighbors.next
if (Math.abs(vertex.x - prev.x) < snapRadius) {
vertex.x = prev.x
} else if (Math.abs(vertex.x - next.x) < snapRadius) {
vertex.x = next.x
}
if (Math.abs(vertex.y - prev.y) < snapRadius) {
vertex.y = neighbors.prev.y
} else if (Math.abs(vertex.y - next.y) < snapRadius) {
vertex.y = next.y
}
}
}
protected onHandleRemove({ handle, e }: EventArgs['remove']) {
if (this.options.removable) {
const index = handle.options.index
const edgeView = this.cellView
edgeView.cell.removeVertexAt(index, { ui: true })
if (this.options.addable) {
this.updatePath()
}
edgeView.checkMouseleave(this.normalizeEvent(e))
}
}
protected allowAddVertex(e: Dom.MouseDownEvent) {
const guard = this.guard(e)
const addable = this.options.addable && this.cellView.can('vertexAddable')
const matchModifiers = this.options.modifiers
? isModifierKeyMatch(e, this.options.modifiers)
: true
return !guard && addable && matchModifiers
}
protected onPathMouseDown(evt: Dom.MouseDownEvent) {
const edgeView = this.cellView
if (!this.allowAddVertex(evt)) {
return
}
evt.stopPropagation()
evt.preventDefault()
const e = this.normalizeEvent(evt)
const vertex = this.graph.snapToGrid(e.clientX, e.clientY).toJSON()
edgeView.cell.startBatch('add-vertex', { ui: true, toolId: this.cid })
const index = edgeView.getVertexIndex(vertex.x, vertex.y)
this.snapVertex(vertex, index)
edgeView.cell.insertVertex(vertex, index, {
ui: true,
toolId: this.cid,
})
this.render()
const handle = this.handles[index]
this.eventData(e, { vertexAdded: true })
handle.onMouseDown(e)
}
protected onRemove() {
this.resetHandles()
}
}
interface Options extends ToolItemOptions {
snapRadius?: number
addable?: boolean
removable?: boolean
removeRedundancies?: boolean
stopPropagation?: boolean
modifiers?: string | ModifierKey[]
attrs?: SimpleAttrs | ((handle: Handle) => SimpleAttrs)
createHandle?: (options: HandleOptions) => Handle
processHandle?: (handle: Handle) => void
onChanged?: (options: { edge: Edge; edgeView: EdgeView }) => void
}
export class Handle extends View<EventArgs> {
protected get graph() {
return this.options.graph
}
constructor(public readonly options: HandleOptions) {
super()
this.render()
this.delegateEvents({
mousedown: 'onMouseDown',
touchstart: 'onMouseDown',
dblclick: 'onDoubleClick',
})
}
render() {
this.container = createViewElement('circle', true)
const attrs = this.options.attrs
if (typeof attrs === 'function') {
const defaults = Vertices.getDefaults<Options>()
this.setAttrs({
...defaults.attrs,
...attrs(this),
})
} else {
this.setAttrs(attrs)
}
this.addClass(this.prefixClassName('edge-tool-vertex'))
}
updatePosition(x: number, y: number) {
this.setAttrs({ cx: x, cy: y })
}
onMouseDown(evt: Dom.MouseDownEvent) {
if (this.options.guard(evt)) {
return
}
evt.stopPropagation()
evt.preventDefault()
this.graph.view.undelegateEvents()
this.delegateDocumentEvents(
{
mousemove: 'onMouseMove',
touchmove: 'onMouseMove',
mouseup: 'onMouseUp',
touchend: 'onMouseUp',
touchcancel: 'onMouseUp',
},
evt.data,
)
this.emit('change', { e: evt, handle: this })
}
protected onMouseMove(evt: Dom.MouseMoveEvent) {
this.emit('changing', { e: evt, handle: this })
}
protected onMouseUp(evt: Dom.MouseUpEvent) {
this.emit('changed', { e: evt, handle: this })
this.undelegateDocumentEvents()
this.graph.view.delegateEvents()
}
protected onDoubleClick(evt: Dom.DoubleClickEvent) {
this.emit('remove', { e: evt, handle: this })
}
}
interface HandleOptions {
graph: Graph
index: number
guard: (evt: Dom.EventObject) => boolean
attrs: SimpleAttrs | ((handle: Handle) => SimpleAttrs)
}
interface EventArgs {
change: { e: Dom.MouseDownEvent; handle: Handle }
changing: { e: Dom.MouseMoveEvent; handle: Handle }
changed: { e: Dom.MouseUpEvent; handle: Handle }
remove: { e: Dom.DoubleClickEvent; handle: Handle }
}