diff --git a/src/graph-view-node.js b/src/graph-view-node.js index 633c458..b114b19 100644 --- a/src/graph-view-node.js +++ b/src/graph-view-node.js @@ -24,6 +24,9 @@ class GraphViewNode { this.nodeSchema = nodeSchema; this.state = GraphViewNode.STATES.DEFAULT; + // Track event listeners for cleanup + this._inputEventListeners = []; + const rectHeight = this.getSchemaValue('baseHeight'); let portHeight = 0; let attributeHeight = 0; @@ -334,6 +337,10 @@ class GraphViewNode { } input.enabled = !this._graphView._config.readOnly; input.dom.setAttribute('id', `input_${attribute.name}`); + + // Prevent input interactions from triggering node selection + this._preventNodeSelectionOnInput(input); + container.dom.setAttribute('style', `margin-top: ${i === 0 ? 33 + portHeight : 5}px; margin-bottom: 5px;`); container.append(label); container.append(input); @@ -366,6 +373,51 @@ class GraphViewNode { this.model = rect; } + _preventNodeSelectionOnInput(input) { + // Stop event propagation on input elements to prevent JointJS from + // intercepting pointer events, which would prevent input interaction + const stopPropagation = e => e.stopPropagation(); + + const addTrackedListener = (element, eventType, handler) => { + element.addEventListener(eventType, handler); + this._inputEventListeners.push({ element, eventType, handler }); + }; + + // Handle the wrapper element + addTrackedListener(input.dom, 'pointerdown', stopPropagation); + addTrackedListener(input.dom, 'mousedown', stopPropagation); + + // Handle the actual input element(s) + if (input.input) { + addTrackedListener(input.input, 'pointerdown', stopPropagation); + addTrackedListener(input.input, 'mousedown', stopPropagation); + } + + // Handle VectorInput which has multiple sub-inputs + if (input.inputs) { + input.inputs.forEach((subInput) => { + if (subInput.input) { + addTrackedListener(subInput.input, 'pointerdown', stopPropagation); + addTrackedListener(subInput.input, 'mousedown', stopPropagation); + } + }); + } + } + + destroy() { + // Remove all tracked input event listeners to prevent memory leaks + this._inputEventListeners.forEach(({ element, eventType, handler }) => { + element.removeEventListener(eventType, handler); + }); + this._inputEventListeners = []; + + // Clean up context menu if it exists + if (this._contextMenu) { + this._contextMenu.destroy(); + this._contextMenu = null; + } + } + getSchemaValue(item) { return this.nodeSchema[item] !== undefined ? this.nodeSchema[item] : this._config.defaultStyles.node[item]; } diff --git a/src/graph-view.js b/src/graph-view.js index 98b2c5d..a6bdfef 100644 --- a/src/graph-view.js +++ b/src/graph-view.js @@ -222,9 +222,13 @@ class GraphView extends JointGraph { removeNode(modelId) { const node = this.getNode(modelId); - this._graph.removeCells(node.model); - delete this._nodes[node.nodeData.id]; - delete this._nodes[modelId]; + if (node) { + // Clean up node resources before removal + node.destroy(); + this._graph.removeCells(node.model); + delete this._nodes[node.nodeData.id]; + delete this._nodes[modelId]; + } } updateNodeAttribute(id, attribute, value) { @@ -439,6 +443,14 @@ class GraphView extends JointGraph { } destroy() { + // Clean up all nodes before clearing the graph + Object.values(this._nodes).forEach((node) => { + if (node) { + node.destroy(); + } + }); + this._nodes = {}; + this._graph.clear(); this._paper.remove(); }