UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

1,295 lines (1,134 loc) 35.3 kB
import { ArrayExt, Dom, FunctionExt } from '../../common' import { Config } from '../../config' import { Point, Rectangle, type RectangleLike, snapToGrid, } from '../../geometry' import { Cell } from '../../model/cell' import type { Edge } from '../../model/edge' import type { Node } from '../../model/node' import type { Label, Port } from '../../model/port' import type { CellAttrs, PortLayoutResult } from '../../registry' import type { KeyPoint } from '../../types' import type { AttrManagerUpdateOptions } from '../attr' import { CellView } from '../cell' import type { EdgeView } from '../edge' import { Markup, type MarkupJSONMarkup, type MarkupSelectors } from '../markup' import type { EventDataMagnet, EventDataMousemove, EventDataMoving, EventDataMovingTargetNode, NodeViewEventArgs, NodeViewMouseEventArgs, NodeViewOptions, NodeViewPortCache, NodeViewPositionEventArgs, } from './type' export * from './type' export class NodeView< Entity extends Node = Node, Options extends NodeViewOptions = NodeViewOptions, > extends CellView<Entity, Options> { protected portsCache: { [id: string]: NodeViewPortCache } = {} public static isNodeView(instance: any): instance is NodeView { if (instance == null) { return false } if (instance instanceof NodeView) { return true } const tag = instance[Symbol.toStringTag] const view = instance as NodeView if ( (tag == null || tag === NodeViewToStringTag) && typeof view.isNodeView === 'function' && typeof view.isEdgeView === 'function' && typeof view.confirmUpdate === 'function' && typeof view.update === 'function' && typeof view.findPortElem === 'function' && typeof view.resize === 'function' && typeof view.rotate === 'function' && typeof view.translate === 'function' ) { return true } return false } protected get [Symbol.toStringTag]() { return NodeViewToStringTag } protected getContainerClassName() { const classList = [ super.getContainerClassName(), this.prefixClassName('node'), ] if (!this.can('nodeMovable')) { classList.push(this.prefixClassName('node-immovable')) } return classList.join(' ') } protected updateClassName(e: Dom.MouseEnterEvent) { const target = e.target if (target.hasAttribute('magnet')) { // port const className = this.prefixClassName('port-unconnectable') if (this.can('magnetConnectable')) { Dom.removeClass(target, className) } else { Dom.addClass(target, className) } } else { // node const className = this.prefixClassName('node-immovable') if (this.can('nodeMovable')) { this.removeClass(className) } else { this.addClass(className) } } } isNodeView(): this is NodeView { return true } confirmUpdate(flag: number, options: any = {}) { let ret = flag const toolRelatedGeometryChanged = this.hasAction(flag, [ 'resize', 'translate', 'rotate', ]) if (this.hasAction(ret, 'ports')) { this.removePorts() this.cleanPortsCache() } if (this.hasAction(ret, 'render')) { this.render() ret = this.removeAction(ret, [ 'render', 'update', 'resize', 'translate', 'rotate', 'ports', 'tools', ]) } else { ret = this.handleAction( ret, 'resize', () => this.resize(), 'update', // Resize method is calling `update()` internally ) ret = this.handleAction( ret, 'update', () => this.update(), // `update()` will render ports when useCSSSelectors are enabled Config.useCSSSelector ? 'ports' : null, ) ret = this.handleAction(ret, 'translate', () => this.translate()) ret = this.handleAction(ret, 'rotate', () => this.rotate()) ret = this.handleAction(ret, 'ports', () => this.renderPorts()) ret = this.handleAction(ret, 'tools', () => { const hasModelTools = this.cell.getTools() != null if ( !toolRelatedGeometryChanged || this.tools == null || !hasModelTools ) { this.renderTools() } else { this.updateTools(options) } }) } return ret } update(partialAttrs?: CellAttrs) { this.cleanCache() // When CSS selector strings are used, make sure no rule matches port nodes. if (Config.useCSSSelector) { this.removePorts() } const node = this.cell const size = node.getSize() const attrs = node.getAttrs() this.updateAttrs(this.container, attrs, { attrs: partialAttrs === attrs ? null : partialAttrs, rootBBox: new Rectangle(0, 0, size.width, size.height), selectors: this.selectors, }) if (Config.useCSSSelector) { this.renderPorts() } } protected renderMarkup() { const markup = this.cell.markup if (markup) { if (typeof markup === 'string') { throw new TypeError('Not support string markup.') } return this.renderJSONMarkup(markup) } throw new TypeError('Invalid node markup.') } protected renderJSONMarkup(markup: MarkupJSONMarkup | MarkupJSONMarkup[]) { const ret = this.parseJSONMarkup(markup, this.container) this.selectors = ret.selectors this.container.appendChild(ret.fragment) } render() { this.empty() this.renderMarkup() this.resize() this.updateTransform() if (!Config.useCSSSelector) { this.renderPorts() } this.renderTools() this.notify('view:render', { view: this }) return this } resize() { if (this.cell.getAngle()) { this.rotate() } this.update() } translate() { this.updateTransform() } rotate() { this.updateTransform() } protected getTranslationString() { const position = this.cell.getPosition() return `translate(${position.x},${position.y})` } protected getRotationString() { const angle = this.cell.getAngle() if (angle) { const size = this.cell.getSize() return `rotate(${angle},${size.width / 2},${size.height / 2})` } } protected updateTransform() { let transform = this.getTranslationString() const rot = this.getRotationString() if (rot) { transform += ` ${rot}` } this.container.setAttribute('transform', transform) } // #region ports findPortElem(portId?: string, selector?: string) { const cache = portId ? this.portsCache[portId] : null if (!cache) { return null } const portRoot = cache.portContentElement const portSelectors = cache.portContentSelectors || {} return this.findOne(selector, portRoot, portSelectors) } protected cleanPortsCache() { this.portsCache = {} } protected removePorts() { Object.values(this.portsCache).forEach((cached) => { Dom.remove(cached.portElement) }) } protected renderPorts() { const container = this.container // References to rendered elements without z-index const references: Element[] = [] container.childNodes.forEach((child) => { references.push(child as Element) }) const parsedPorts = this.cell.getParsedPorts() const portsGropsByZ = ArrayExt.groupBy(parsedPorts, 'zIndex') const autoZIndexKey = 'auto' // render non-z first if (portsGropsByZ[autoZIndexKey]) { portsGropsByZ[autoZIndexKey].forEach((port: Port) => { const portElement = this.getPortElement(port) container.append(portElement) references.push(portElement) }) } Object.keys(portsGropsByZ).forEach((key) => { if (key !== autoZIndexKey) { const zIndex = parseInt(key, 10) this.appendPorts(portsGropsByZ[key], zIndex, references) } }) this.updatePorts() this.cleanCache() this.updateConnectedEdges() } protected updateConnectedEdges() { const graph = this.graph const node = this.cell const edges = graph.model.getConnectedEdges(node) for (let i = 0, n = edges.length; i < n; i += 1) { const edge = edges[i] const edgeView = edge.findView(graph) as EdgeView if (!edgeView || !graph.renderer.isViewMounted(edgeView)) { continue } const actions = ['update'] if (edge.getSourceCell() === node) { actions.push('source') } if (edge.getTargetCell() === node) { actions.push('target') } graph.renderer.requestViewUpdate( edgeView, edgeView.getFlag(actions as any), ) } } protected appendPorts(ports: Port[], zIndex: number, refs: Element[]) { const elems = ports.map((p) => this.getPortElement(p)) if (refs[zIndex] || zIndex < 0) { Dom.before(refs[Math.max(zIndex, 0)], elems) } else { Dom.append(this.container, elems) } } protected getPortElement(port: Port) { const cached = this.portsCache[port.id] if (cached) { return cached.portElement } return this.createPortElement(port) } protected createPortElement(port: Port) { let renderResult = Markup.renderMarkup(this.cell.getPortContainerMarkup()) const portElement = renderResult.elem if (portElement == null) { throw new Error('Invalid port container markup.') } renderResult = Markup.renderMarkup(this.getPortMarkup(port)) const portContentElement = renderResult.elem const portContentSelectors = renderResult.selectors if (portContentElement == null) { throw new Error('Invalid port markup.') } this.setAttrs( { port: port.id, 'port-group': port.group, }, portContentElement, ) let portClass = 'x6-port' if (port.group) { portClass += ` x6-port-${port.group}` } Dom.addClass(portElement, portClass) Dom.addClass(portElement, 'x6-port') Dom.addClass(portContentElement, 'x6-port-body') portElement.appendChild(portContentElement) let portSelectors: MarkupSelectors | undefined = portContentSelectors let portLabelElement: Element | undefined let portLabelSelectors: MarkupSelectors | null | undefined const existLabel = this.existPortLabel(port) if (existLabel) { renderResult = Markup.renderMarkup(this.getPortLabelMarkup(port.label)) portLabelElement = renderResult.elem portLabelSelectors = renderResult.selectors if (portLabelElement == null) { throw new Error('Invalid port label markup.') } if (portContentSelectors && portLabelSelectors) { // eslint-disable-next-line for (const key in portLabelSelectors) { if (portContentSelectors[key] && key !== this.rootSelector) { throw new Error('Selectors within port must be unique.') } } portSelectors = { ...portContentSelectors, ...portLabelSelectors, } } Dom.addClass(portLabelElement, 'x6-port-label') portElement.appendChild(portLabelElement) } this.portsCache[port.id] = { portElement, portSelectors, portLabelElement, portLabelSelectors, portContentElement, portContentSelectors, } if (this.graph.options.onPortRendered) { this.graph.options.onPortRendered({ port, node: this.cell, container: portElement, selectors: portSelectors, labelContainer: portLabelElement, labelSelectors: portLabelSelectors, contentContainer: portContentElement, contentSelectors: portContentSelectors, }) } return portElement } protected updatePorts() { const groups = this.cell.getParsedGroups() const groupList = Object.keys(groups) if (groupList.length === 0) { this.updatePortGroup() } else { groupList.forEach((groupName) => { this.updatePortGroup(groupName) }) } } protected updatePortGroup(groupName?: string) { const bbox = Rectangle.fromSize(this.cell.getSize()) const metrics = this.cell.getPortsLayoutByGroup(groupName, bbox) for (let i = 0, n = metrics.length; i < n; i += 1) { const metric = metrics[i] const portId = metric.portId const cached = this.portsCache[portId] || {} const portLayout = metric.portLayout // @ts-expect-error this.applyPortTransform(cached.portElement, portLayout) if (metric.portAttrs != null) { const options: Partial<AttrManagerUpdateOptions> = { // @ts-expect-error selectors: cached.portSelectors || {}, } if (metric.portSize) { options.rootBBox = Rectangle.fromSize(metric.portSize) } // @ts-expect-error this.updateAttrs(cached.portElement, metric.portAttrs, options) } const labelLayout = metric.labelLayout // @ts-expect-error if (labelLayout && cached.portLabelElement) { this.applyPortTransform( // @ts-expect-error cached.portLabelElement, labelLayout, -(portLayout.angle || 0), ) if (labelLayout.attrs) { const options: Partial<AttrManagerUpdateOptions> = { // @ts-expect-error selectors: cached.portLabelSelectors || {}, } if (metric.labelSize) { options.rootBBox = Rectangle.fromSize(metric.labelSize) } // @ts-expect-error this.updateAttrs(cached.portLabelElement, labelLayout.attrs, options) } } } } protected applyPortTransform( element: Element, layout: PortLayoutResult, initialAngle = 0, ) { const angle = layout.angle const position = layout.position const matrix = Dom.createSVGMatrix() .rotate(initialAngle) .translate(position.x || 0, position.y || 0) .rotate(angle || 0) Dom.transform(element as SVGElement, matrix, { absolute: true }) } protected getPortMarkup(port: Port) { return port.markup || this.cell.portMarkup } protected getPortLabelMarkup(label: Label) { return label.markup || this.cell.portLabelMarkup } protected existPortLabel(port: Port) { return port.attrs && port.attrs.text } // #endregion // #region events protected getEventArgs<E>(e: E): NodeViewMouseEventArgs<E> protected getEventArgs<E>( e: E, x: number, y: number, ): NodeViewPositionEventArgs<E> protected getEventArgs<E>(e: E, x?: number, y?: number) { const view = this // eslint-disable-line const node = view.cell const cell = node if (x == null || y == null) { return { e, view, node, cell } as NodeViewMouseEventArgs<E> } return { e, x, y, view, node, cell } as NodeViewPositionEventArgs<E> } protected getPortEventArgs<E>( e: E, port: string, pos?: { x: number; y: number }, ): NodeViewPositionEventArgs<E> | NodeViewMouseEventArgs<E> { const view = this // eslint-disable-line const node = view.cell const cell = node if (pos) { return { e, x: pos.x, y: pos.y, view, node, cell, port, } as NodeViewPositionEventArgs<E> } return { e, view, node, cell, port } as NodeViewMouseEventArgs<E> } notifyMouseDown(e: Dom.MouseDownEvent, x: number, y: number) { super.onMouseDown(e, x, y) this.notify('node:mousedown', this.getEventArgs(e, x, y)) } notifyMouseMove(e: Dom.MouseMoveEvent, x: number, y: number) { super.onMouseMove(e, x, y) this.notify('node:mousemove', this.getEventArgs(e, x, y)) } notifyMouseUp(e: Dom.MouseUpEvent, x: number, y: number) { super.onMouseUp(e, x, y) this.notify('node:mouseup', this.getEventArgs(e, x, y)) } notifyPortEvent( name: string, e: Dom.EventObject, pos?: { x: number; y: number }, ) { const port = this.findAttr('port', e.target) if (port) { const originType = e.type if (name === 'node:port:mouseenter') { e.type = 'mouseenter' } else if (name === 'node:port:mouseleave') { e.type = 'mouseleave' } this.notify(name, this.getPortEventArgs(e, port, pos)) e.type = originType } } onClick(e: Dom.ClickEvent, x: number, y: number) { super.onClick(e, x, y) this.notify('node:click', this.getEventArgs(e, x, y)) this.notifyPortEvent('node:port:click', e, { x, y }) } onDblClick(e: Dom.DoubleClickEvent, x: number, y: number) { super.onDblClick(e, x, y) this.notify('node:dblclick', this.getEventArgs(e, x, y)) this.notifyPortEvent('node:port:dblclick', e, { x, y }) } onContextMenu(e: Dom.ContextMenuEvent, x: number, y: number) { super.onContextMenu(e, x, y) this.notify('node:contextmenu', this.getEventArgs(e, x, y)) this.notifyPortEvent('node:port:contextmenu', e, { x, y }) } onMouseDown(e: Dom.MouseDownEvent, x: number, y: number) { if (this.isPropagationStopped(e)) { return } this.notifyMouseDown(e, x, y) this.notifyPortEvent('node:port:mousedown', e, { x, y }) this.startNodeDragging(e, x, y) } onMouseMove(e: Dom.MouseMoveEvent, x: number, y: number) { const data = this.getEventData<EventDataMousemove>(e) const action = data.action if (action === 'magnet') { this.dragMagnet(e, x, y) } else { if (action === 'move') { const meta = data as EventDataMoving const view = meta.targetView || this view.dragNode(e, x, y) view.notify('node:moving', { e, x, y, view, cell: view.cell, node: view.cell, }) } this.notifyMouseMove(e, x, y) this.notifyPortEvent('node:port:mousemove', e, { x, y }) } this.setEventData<EventDataMousemove>(e, data) } onMouseUp(e: Dom.MouseUpEvent, x: number, y: number) { const data = this.getEventData<EventDataMousemove>(e) const action = data.action if (action === 'magnet') { this.stopMagnetDragging(e, x, y) } else { this.notifyMouseUp(e, x, y) this.notifyPortEvent('node:port:mouseup', e, { x, y }) if (action === 'move') { const meta = data as EventDataMoving const view = meta.targetView || this view.stopNodeDragging(e, x, y) } } const magnet = (data as EventDataMagnet).targetMagnet if (magnet) { this.onMagnetClick(e, magnet, x, y) } this.checkMouseleave(e) } onMouseOver(e: Dom.MouseOverEvent) { super.onMouseOver(e) this.notify('node:mouseover', this.getEventArgs(e)) // mock mouseenter event,so we can get correct trigger time when move mouse from node to port // wo also need to change e.type for use get correct event args this.notifyPortEvent('node:port:mouseenter', e) this.notifyPortEvent('node:port:mouseover', e) } onMouseOut(e: Dom.MouseOutEvent) { super.onMouseOut(e) this.notify('node:mouseout', this.getEventArgs(e)) // mock mouseleave event,so we can get correct trigger time when move mouse from port to node // wo also need to change e.type for use get correct event args this.notifyPortEvent('node:port:mouseleave', e) this.notifyPortEvent('node:port:mouseout', e) } onMouseEnter(e: Dom.MouseEnterEvent) { this.updateClassName(e) super.onMouseEnter(e) this.notify('node:mouseenter', this.getEventArgs(e)) } onMouseLeave(e: Dom.MouseLeaveEvent) { super.onMouseLeave(e) this.notify('node:mouseleave', this.getEventArgs(e)) } onMouseWheel(e: Dom.EventObject, x: number, y: number, delta: number) { super.onMouseWheel(e, x, y, delta) this.notify('node:mousewheel', { delta, ...this.getEventArgs(e, x, y), }) } onMagnetClick(e: Dom.MouseUpEvent, magnet: Element, x: number, y: number) { const graph = this.graph const count = graph.view.getMouseMovedCount(e) if (count > graph.options.clickThreshold) { return } this.notify('node:magnet:click', { magnet, ...this.getEventArgs(e, x, y), }) } onMagnetDblClick( e: Dom.DoubleClickEvent, magnet: Element, x: number, y: number, ) { this.notify('node:magnet:dblclick', { magnet, ...this.getEventArgs(e, x, y), }) } onMagnetContextMenu( e: Dom.ContextMenuEvent, magnet: Element, x: number, y: number, ) { this.notify('node:magnet:contextmenu', { magnet, ...this.getEventArgs(e, x, y), }) } onMagnetMouseDown( e: Dom.MouseDownEvent, magnet: Element, x: number, y: number, ) { this.startMagnetDragging(e, x, y) } onCustomEvent(e: Dom.MouseDownEvent, name: string, x: number, y: number) { this.notify('node:customevent', { name, ...this.getEventArgs(e, x, y) }) super.onCustomEvent(e, name, x, y) } protected prepareEmbedding(e: Dom.MouseMoveEvent) { const graph = this.graph const data = this.getEventData<EventDataMovingTargetNode>(e) const node = data.cell || this.cell const view = graph.findViewByCell(node) const localPoint = graph.snapToGrid(e.clientX, e.clientY) this.notify('node:embed', { e, node, view, cell: node, x: localPoint.x, y: localPoint.y, currentParent: node.getParent(), }) } processEmbedding(e: Dom.MouseMoveEvent, data: EventDataMovingTargetNode) { const cell = data.cell || this.cell const graph = data.graph || this.graph const options = graph.options.embedding const findParent = options.findParent let candidates = typeof findParent === 'function' ? ( FunctionExt.call(findParent, graph, { view: this, node: this.cell, }) as Cell[] ).filter((c) => { return ( Cell.isCell(c) && this.cell.id !== c.id && !c.isDescendantOf(this.cell) ) }) : graph.model.getNodesUnderNode(cell, { by: findParent as KeyPoint, }) // Picks the node with the highest `z` index if (options.frontOnly) { if (candidates.length > 0) { const zIndexMap = ArrayExt.groupBy(candidates, 'zIndex') const maxZIndex = ArrayExt.max( Object.keys(zIndexMap).map((z) => parseInt(z, 10)), ) if (maxZIndex) { candidates = zIndexMap[maxZIndex] } } } // Filter the nodes which is invisiable candidates = candidates.filter((candidate) => candidate.visible) let newCandidateView = null const prevCandidateView = data.candidateEmbedView const validateEmbeding = options.validate for (let i = candidates.length - 1; i >= 0; i -= 1) { const candidate = candidates[i] if (prevCandidateView && prevCandidateView.cell.id === candidate.id) { // candidate remains the same newCandidateView = prevCandidateView break } else { const view = candidate.findView(graph) as NodeView if ( validateEmbeding && FunctionExt.call(validateEmbeding, graph, { child: this.cell, parent: view.cell, childView: this, parentView: view, }) ) { // flip to the new candidate newCandidateView = view break } } } this.clearEmbedding(data) if (newCandidateView) { newCandidateView.highlight(null, { type: 'embedding' }) } data.candidateEmbedView = newCandidateView const localPoint = graph.snapToGrid(e.clientX, e.clientY) this.notify('node:embedding', { e, cell, node: cell, view: graph.findViewByCell(cell), x: localPoint.x, y: localPoint.y, currentParent: cell.getParent(), candidateParent: newCandidateView ? newCandidateView.cell : null, }) } clearEmbedding(data: EventDataMovingTargetNode) { const candidateView = data.candidateEmbedView if (candidateView) { candidateView.unhighlight(null, { type: 'embedding' }) data.candidateEmbedView = null } } finalizeEmbedding(e: Dom.MouseUpEvent, data: EventDataMovingTargetNode) { this.graph.startBatch('embedding') const cell = data.cell || this.cell const graph = data.graph || this.graph const view = graph.findViewByCell(cell) const parent = cell.getParent() const candidateView = data.candidateEmbedView if (candidateView) { // Candidate view is chosen to become the parent of the node. candidateView.unhighlight(null, { type: 'embedding' }) data.candidateEmbedView = null if (parent == null || parent.id !== candidateView.cell.id) { candidateView.cell.insertChild(cell, undefined, { ui: true }) } } else if (parent) { parent.unembed(cell, { ui: true }) } graph.model.getConnectedEdges(cell, { deep: true }).forEach((edge) => { edge.updateParent({ ui: true }) }) if (view && candidateView) { const localPoint = graph.snapToGrid(e.clientX, e.clientY) view.notify('node:embedded', { e, cell, x: localPoint.x, y: localPoint.y, node: cell, view: graph.findViewByCell(cell), previousParent: parent, currentParent: cell.getParent(), }) } this.graph.stopBatch('embedding') } getDelegatedView() { let cell = this.cell let view: NodeView = this // eslint-disable-line while (view) { if (cell.isEdge()) { break } if (!cell.hasParent() || view.can('stopDelegateOnDragging')) { return view } cell = cell.getParent() as Entity view = this.graph.findViewByCell(cell) as NodeView } return null } protected validateMagnet( cellView: CellView, magnet: Element, e: Dom.MouseDownEvent | Dom.MouseEnterEvent, ) { if (magnet.getAttribute('magnet') !== 'passive') { const validate = this.graph.options.connecting.validateMagnet if (validate) { return FunctionExt.call(validate, this.graph, { e, magnet, view: cellView, cell: cellView.cell, }) } return true } return false } protected startMagnetDragging(e: Dom.MouseDownEvent, x: number, y: number) { if (!this.can('magnetConnectable')) { return } e.stopPropagation() const magnet = e.currentTarget const graph = this.graph this.setEventData<Partial<EventDataMagnet>>(e, { targetMagnet: magnet, }) if (this.validateMagnet(this, magnet, e)) { // @ts-expect-error if (graph.options.magnetThreshold <= 0) { this.startConnectting(e, magnet, x, y) } this.setEventData<Partial<EventDataMagnet>>(e, { action: 'magnet', }) this.stopPropagation(e) } else { this.onMouseDown(e, x, y) } graph.view.delegateDragEvents(e, this) } protected startConnectting( e: Dom.MouseDownEvent, magnet: Element, x: number, y: number, ) { this.graph.model.startBatch('add-edge') const edgeView = this.createEdgeFromMagnet(magnet, x, y) edgeView.setEventData( e, edgeView.prepareArrowheadDragging('target', { x, y, isNewEdge: true, fallbackAction: 'remove', }), ) this.setEventData<Partial<EventDataMagnet>>(e, { edgeView }) edgeView.notifyMouseDown(e, x, y) } protected getDefaultEdge(sourceView: CellView, sourceMagnet: Element) { let edge: Edge | undefined | null | void const create = this.graph.options.connecting.createEdge if (create) { edge = FunctionExt.call(create, this.graph, { sourceMagnet, sourceView, sourceCell: sourceView.cell, }) } return edge as Edge } protected createEdgeFromMagnet(magnet: Element, x: number, y: number) { const graph = this.graph const model = graph.model const edge = this.getDefaultEdge(this, magnet) edge.setSource({ ...edge.getSource(), ...this.getEdgeTerminal(magnet, x, y, edge, 'source'), }) edge.setTarget({ ...edge.getTarget(), x, y }) edge.addTo(model, { async: false, ui: true }) return edge.findView(graph) as EdgeView } protected dragMagnet(e: Dom.MouseMoveEvent, x: number, y: number) { const data = this.getEventData<EventDataMagnet>(e) const edgeView = data.edgeView if (edgeView) { edgeView.onMouseMove(e, x, y) this.autoScrollGraph(e.clientX, e.clientY) } else { const graph = this.graph const magnetThreshold = graph.options.magnetThreshold as any const currentTarget = this.getEventTarget(e) const targetMagnet = data.targetMagnet // magnetThreshold when the pointer leaves the magnet if (magnetThreshold === 'onleave') { if ( targetMagnet === currentTarget || targetMagnet.contains(currentTarget) ) { return } // eslint-disable-next-line no-lonely-if } else { // magnetThreshold defined as a number of movements if (graph.view.getMouseMovedCount(e) <= magnetThreshold) { return } } this.startConnectting(e as any, targetMagnet, x, y) } } protected stopMagnetDragging(e: Dom.MouseUpEvent, x: number, y: number) { const data = this.eventData<EventDataMagnet>(e) const edgeView = data.edgeView if (edgeView) { edgeView.onMouseUp(e, x, y) this.graph.model.stopBatch('add-edge') } } protected notifyUnhandledMouseDown( e: Dom.MouseDownEvent, x: number, y: number, ) { this.notify('node:unhandled:mousedown', { e, x, y, view: this, cell: this.cell, node: this.cell, }) } protected notifyNodeMove<Key extends keyof NodeViewEventArgs>( name: Key, e: Dom.MouseMoveEvent | Dom.MouseUpEvent, x: number, y: number, cell: Cell, ) { let cells = [cell] const selection = this.graph.getPlugin<any>('selection') if (selection && selection.isSelectionMovable()) { const selectedCells = selection.getSelectedCells() if (selectedCells.includes(cell)) { cells = selectedCells.filter((c: Cell) => c.isNode()) } } cells.forEach((c: Cell) => { this.notify(name, { e, x, y, cell: c, node: c, view: c.findView(this.graph), }) }) } protected getRestrictArea(view?: NodeView): RectangleLike | null { const restrict = this.graph.options.translating.restrict const area = typeof restrict === 'function' ? FunctionExt.call(restrict, this.graph, view!) : restrict if (typeof area === 'number') { return this.graph.transform.getGraphArea().inflate(area) } if (area === true) { return this.graph.transform.getGraphArea() } return area || null } protected startNodeDragging(e: Dom.MouseDownEvent, x: number, y: number) { const targetView = this.getDelegatedView() if (targetView == null || !targetView.can('nodeMovable')) { return this.notifyUnhandledMouseDown(e, x, y) } this.setEventData<EventDataMoving>(e, { targetView, action: 'move', }) const position = Point.create(targetView.cell.getPosition()) targetView.setEventData<EventDataMovingTargetNode>(e, { moving: false, offset: position.diff(x, y), restrict: this.getRestrictArea(targetView), }) } protected dragNode(e: Dom.MouseMoveEvent, x: number, y: number) { const node = this.cell const graph = this.graph const gridSize = graph.getGridSize() const data = this.getEventData<EventDataMovingTargetNode>(e) const offset = data.offset const restrict = data.restrict if (!data.moving) { data.moving = true this.addClass('node-moving') this.notifyNodeMove('node:move', e, x, y, this.cell) } this.autoScrollGraph(e.clientX, e.clientY) const posX = snapToGrid(x + offset.x, gridSize) const posY = snapToGrid(y + offset.y, gridSize) node.setPosition(posX, posY, { restrict, deep: true, ui: true, }) if (graph.options.embedding.enabled) { if (!data.embedding) { this.prepareEmbedding(e) data.embedding = true } this.processEmbedding(e, data) } } protected autoOffsetNode() { const node = this.cell const graph = this.graph const nodePosition = { id: node.id, ...node.getPosition() } const allNodes = graph.getNodes() const restNodePositions = allNodes .map((node) => { const pos = node.getPosition() return { id: node.id, x: pos.x, y: pos.y } }) .filter((pos) => { return pos.id !== nodePosition.id }) /** offset directions: right bottom, right top, left bottom, left top */ const directions = [ [1, 1], // offset to right bottom [1, -1], // offset to right top [-1, 1], // offset to left bottom [-1, -1], // offset to left top ] let step = graph.getGridSize() const hasSamePosition = (position: { x: number; y: number }) => restNodePositions.some((pos) => { return pos.x === position.x && pos.y === position.y }) while (hasSamePosition(nodePosition)) { let found = false for (let i = 0; i < directions.length; i += 1) { const dir = directions[i] const position = { x: nodePosition.x + dir[0] * step, y: nodePosition.y + dir[1] * step, } if (!hasSamePosition(position)) { node.translate(dir[0] * step, dir[1] * step) found = true break } } if (found) { break } step += graph.getGridSize() } } protected stopNodeDragging(e: Dom.MouseUpEvent, x: number, y: number) { const data = this.getEventData<EventDataMovingTargetNode>(e) const graph = this.graph if (data.embedding) { this.finalizeEmbedding(e, data) } if (data.moving) { const autoOffset = graph.options.translating.autoOffset if (autoOffset) { this.autoOffsetNode() } this.removeClass('node-moving') this.notifyNodeMove('node:moved', e, x, y, this.cell) } data.moving = false data.embedding = false } // eslint-disable-next-line protected autoScrollGraph(x: number, y: number) { const scroller = this.graph.getPlugin<any>('scroller') if (scroller) { scroller.autoScroll(x, y) } } // #endregion } export const NodeViewToStringTag = `X6.${NodeView.name}` NodeView.config({ isSvgElement: true, priority: 0, bootstrap: ['render'], actions: { view: ['render'], markup: ['render'], attrs: ['update'], size: ['resize', 'ports', 'tools'], angle: ['rotate', 'tools'], position: ['translate', 'tools'], ports: ['ports'], tools: ['tools'], }, }) NodeView.registry.register('node', NodeView, true)