Initial commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user