UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

420 lines (364 loc) 11.6 kB
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 } }