// SVG Editor JavaScript Module for LayoutEditor // Handles mouse events, keyboard shortcuts, and coordinate transformations let svgElement = null; let dotNetRef = null; let isInitialized = false; /** * Initialize the SVG editor with event listeners * @param {SVGElement} svg - The SVG element * @param {DotNetObjectReference} dotNet - Reference to Blazor component */ export function initEditor(svg, dotNet) { svgElement = svg; dotNetRef = dotNet; if (!svgElement || !dotNetRef) { console.error('SVG Editor: Invalid initialization parameters'); return; } // Mouse events svgElement.addEventListener('mousemove', handleMouseMove); svgElement.addEventListener('mousedown', handleMouseDown); svgElement.addEventListener('mouseup', handleMouseUp); svgElement.addEventListener('wheel', handleWheel, { passive: false }); svgElement.addEventListener('contextmenu', handleContextMenu); // Keyboard events (on document to capture even when SVG not focused) document.addEventListener('keydown', handleKeyDown); // Prevent default drag behavior svgElement.addEventListener('dragstart', (e) => e.preventDefault()); isInitialized = true; console.log('SVG Editor initialized'); } /** * Dispose the editor and remove event listeners */ export function disposeEditor() { if (svgElement) { svgElement.removeEventListener('mousemove', handleMouseMove); svgElement.removeEventListener('mousedown', handleMouseDown); svgElement.removeEventListener('mouseup', handleMouseUp); svgElement.removeEventListener('wheel', handleWheel); svgElement.removeEventListener('contextmenu', handleContextMenu); } document.removeEventListener('keydown', handleKeyDown); svgElement = null; dotNetRef = null; isInitialized = false; console.log('SVG Editor disposed'); } /** * Convert screen coordinates to SVG coordinates * @param {number} screenX - Screen X coordinate * @param {number} screenY - Screen Y coordinate * @returns {{x: number, y: number}} SVG coordinates */ function screenToSvg(screenX, screenY) { if (!svgElement) return { x: 0, y: 0 }; const pt = svgElement.createSVGPoint(); pt.x = screenX; pt.y = screenY; const ctm = svgElement.getScreenCTM(); if (!ctm) return { x: 0, y: 0 }; const svgPt = pt.matrixTransform(ctm.inverse()); return { x: svgPt.x, y: svgPt.y }; } /** * Export screenToSvg for use from Blazor * @param {number} screenX - Screen X coordinate * @param {number} screenY - Screen Y coordinate * @returns {number[]} SVG coordinates as [x, y] */ export function screenToSvgArray(screenX, screenY) { const coords = screenToSvg(screenX, screenY); return [coords.x, coords.y]; } /** * Handle mouse move event * @param {MouseEvent} e */ function handleMouseMove(e) { if (!dotNetRef) return; const svgCoords = screenToSvg(e.clientX, e.clientY); // Also pass screen coordinates for panning and ctrl key state dotNetRef.invokeMethodAsync('OnMouseMove', svgCoords.x, svgCoords.y, e.clientX, e.clientY, e.ctrlKey); } /** * Handle mouse down event * @param {MouseEvent} e */ function handleMouseDown(e) { if (!dotNetRef) return; const svgCoords = screenToSvg(e.clientX, e.clientY); // For left clicks, always pass to Blazor // For middle/right clicks, filter out node/edge clicks const target = e.target; if (e.button !== 0 && // Not left click (target.classList.contains('node') || target.classList.contains('edge') || target.classList.contains('edge-arrow'))) { return; } // Pass all left clicks to Blazor (CreateEdge mode needs clicks anywhere) // Also pass screen coordinates for panning dotNetRef.invokeMethodAsync('OnMouseDown', svgCoords.x, svgCoords.y, e.button, e.ctrlKey, e.clientX, e.clientY); } /** * Handle mouse up event * @param {MouseEvent} e */ function handleMouseUp(e) { if (!dotNetRef) return; const svgCoords = screenToSvg(e.clientX, e.clientY); dotNetRef.invokeMethodAsync('OnMouseUp', svgCoords.x, svgCoords.y, e.button); } /** * Handle mouse wheel event (zoom) * @param {WheelEvent} e */ function handleWheel(e) { if (!dotNetRef) return; e.preventDefault(); const svgCoords = screenToSvg(e.clientX, e.clientY); dotNetRef.invokeMethodAsync('OnWheel', svgCoords.x, svgCoords.y, e.deltaY); } /** * Handle context menu (right click) * @param {MouseEvent} e */ function handleContextMenu(e) { // Prevent default context menu e.preventDefault(); // TODO: Show custom context menu } /** * Handle keyboard events * @param {KeyboardEvent} e */ function handleKeyDown(e) { if (!dotNetRef) return; // Only handle if not typing in an input const activeElement = document.activeElement; if (activeElement && ( activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable)) { return; } // Check for shortcuts if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 'z': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 'z', true); break; case 'y': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 'y', true); break; case 's': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 's', true); break; case 'c': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 'c', true); break; case 'm': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 'm', true); break; } } else { switch (e.key) { case 'Delete': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 'Delete', false); break; case 'Escape': e.preventDefault(); dotNetRef.invokeMethodAsync('OnKeyDown', 'Escape', false); break; } } } /** * Utility: Get current viewBox values * @returns {{x: number, y: number, width: number, height: number}} */ export function getViewBox() { if (!svgElement) return { x: 0, y: 0, width: 100, height: 100 }; const viewBox = svgElement.viewBox.baseVal; return { x: viewBox.x, y: viewBox.y, width: viewBox.width, height: viewBox.height }; } /** * Utility: Set viewBox values * @param {number} x * @param {number} y * @param {number} width * @param {number} height */ export function setViewBox(x, y, width, height) { if (!svgElement) return; svgElement.setAttribute('viewBox', `${x} ${y} ${width} ${height}`); }