|
22 | 22 | // Track drag state to prevent click after drag |
23 | 23 | let isDragging = $state(false); |
24 | 24 |
|
| 25 | + // Drag preview - rendered off-screen, used as drag image |
| 26 | + let dragPreviewNode = $state<NodeTypeDefinition | null>(null); |
| 27 | + let dragPreviewElement: HTMLDivElement; |
| 28 | +
|
25 | 29 | // Collapsed categories |
26 | 30 | let collapsedCategories = $state<Set<string>>(new Set()); |
27 | 31 |
|
|
85 | 89 | return result; |
86 | 90 | }); |
87 | 91 |
|
| 92 | + // Handle mouse enter to prepare drag preview |
| 93 | + function handleMouseEnter(node: NodeTypeDefinition) { |
| 94 | + dragPreviewNode = node; |
| 95 | + } |
| 96 | +
|
88 | 97 | // Handle drag start |
89 | 98 | function handleDragStart(event: DragEvent, nodeType: NodeTypeDefinition) { |
90 | 99 | isDragging = true; |
91 | 100 | if (event.dataTransfer) { |
92 | 101 | event.dataTransfer.setData('application/pathview-node', nodeType.type); |
93 | 102 | event.dataTransfer.effectAllowed = 'copy'; |
| 103 | +
|
| 104 | + // Use the pre-rendered preview as drag image, centered on cursor |
| 105 | + if (dragPreviewElement) { |
| 106 | + const rect = dragPreviewElement.getBoundingClientRect(); |
| 107 | + event.dataTransfer.setDragImage( |
| 108 | + dragPreviewElement, |
| 109 | + rect.width / 2, |
| 110 | + rect.height / 2 |
| 111 | + ); |
| 112 | + } |
94 | 113 | } |
95 | 114 | } |
96 | 115 |
|
|
99 | 118 | // Reset after a short delay to prevent click from firing |
100 | 119 | setTimeout(() => { |
101 | 120 | isDragging = false; |
| 121 | + dragPreviewNode = null; |
102 | 122 | }, 100); |
103 | 123 | } |
104 | 124 |
|
|
196 | 216 | class="node-tile" |
197 | 217 | class:selected={isSelected(node)} |
198 | 218 | draggable="true" |
| 219 | + onmouseenter={() => handleMouseEnter(node)} |
199 | 220 | ondragstart={(e) => handleDragStart(e, node)} |
200 | 221 | ondragend={handleDragEnd} |
201 | 222 | onclick={() => handleNodeClick(node)} |
|
219 | 240 | <span>Click or drag to add</span> |
220 | 241 | <span>↑↓ Enter</span> |
221 | 242 | </div> |
| 243 | + |
| 244 | + <!-- Hidden drag preview container (rendered off-screen, used as drag image) --> |
| 245 | + <div class="drag-preview-container" aria-hidden="true"> |
| 246 | + {#if dragPreviewNode} |
| 247 | + <div bind:this={dragPreviewElement} class="drag-preview-wrapper"> |
| 248 | + <NodePreview node={dragPreviewNode} /> |
| 249 | + </div> |
| 250 | + {/if} |
| 251 | + </div> |
222 | 252 | </div> |
223 | 253 |
|
224 | 254 | <style> |
|
380 | 410 | font-size: 10px; |
381 | 411 | color: var(--text-disabled); |
382 | 412 | } |
| 413 | +
|
| 414 | + /* Hidden container for drag preview image */ |
| 415 | + .drag-preview-container { |
| 416 | + position: fixed; |
| 417 | + left: -9999px; |
| 418 | + top: -9999px; |
| 419 | + pointer-events: none; |
| 420 | + } |
| 421 | +
|
| 422 | + .drag-preview-wrapper { |
| 423 | + display: inline-block; |
| 424 | + } |
383 | 425 | </style> |
0 commit comments