@@ -64,7 +64,7 @@ export function LearningMapEditor({
6464 language = "en" ,
6565 onChange,
6666} : LearningMapEditorProps ) {
67- const { screenToFlowPosition } = useReactFlow ( ) ;
67+ const { screenToFlowPosition, zoomIn , zoomOut , setCenter , fitView , getNodes , getEdges } = useReactFlow ( ) ;
6868 const [ roadmapState , setRoadmapState , { undo, redo, canUndo, canRedo, reset, resetInitialState } ] = useUndoable < RoadmapData > ( {
6969 settings : { } ,
7070 version : 1 ,
@@ -77,6 +77,8 @@ export function LearningMapEditor({
7777 const [ nodes , setNodes , onNodesChange ] = useNodesState < NodeData > ( [ ] ) ;
7878 const [ edges , setEdges , onEdgesChange ] = useEdgesState < Edge > ( [ ] ) ;
7979 const [ settings , setSettings ] = useState < Settings > ( { background : { color : "#ffffff" } } ) ;
80+ const [ showGrid , setShowGrid ] = useState ( true ) ;
81+ const [ clipboard , setClipboard ] = useState < { nodes : Node < NodeData > [ ] ; edges : Edge [ ] } | null > ( null ) ;
8082
8183 // Use language from settings if available, otherwise use prop
8284 const effectiveLanguage = settings ?. language || language ;
@@ -89,12 +91,22 @@ export function LearningMapEditor({
8991 { action : t . shortcuts . addTaskNode , shortcut : "Ctrl+A" } ,
9092 { action : t . shortcuts . addTopicNode , shortcut : "Ctrl+O" } ,
9193 { action : t . shortcuts . addImageNode , shortcut : "Ctrl+I" } ,
92- { action : t . shortcuts . addTextNode , shortcut : "Ctrl+X " } ,
94+ { action : t . shortcuts . addTextNode , shortcut : "Ctrl+B " } ,
9395 { action : t . shortcuts . deleteNodeEdge , shortcut : "Delete" } ,
9496 { action : t . shortcuts . togglePreviewMode , shortcut : "Ctrl+P" } ,
9597 { action : t . shortcuts . toggleDebugMode , shortcut : "Ctrl+D" } ,
9698 { action : t . shortcuts . selectMultipleNodes , shortcut : "Ctrl+Click or Shift+Drag" } ,
9799 { action : t . shortcuts . showHelp , shortcut : "Ctrl+? or Help Button" } ,
100+ { action : t . shortcuts . zoomIn , shortcut : "Ctrl++" } ,
101+ { action : t . shortcuts . zoomOut , shortcut : "Ctrl+-" } ,
102+ { action : t . shortcuts . resetZoom , shortcut : "Ctrl+0" } ,
103+ { action : t . shortcuts . fitView , shortcut : "Shift+1" } ,
104+ { action : t . shortcuts . zoomToSelection , shortcut : "Shift+2" } ,
105+ { action : t . shortcuts . toggleGrid , shortcut : "Ctrl+'" } ,
106+ { action : t . shortcuts . resetMap , shortcut : "Ctrl+Delete" } ,
107+ { action : t . shortcuts . cut , shortcut : "Ctrl+X" } ,
108+ { action : t . shortcuts . copy , shortcut : "Ctrl+C" } ,
109+ { action : t . shortcuts . paste , shortcut : "Ctrl+V" } ,
98110 ] ;
99111
100112 const [ helpOpen , setHelpOpen ] = useState ( false ) ;
@@ -500,13 +512,111 @@ export function LearningMapEditor({
500512 setDidUndoRedo ( true ) ;
501513 } , [ reset ] ) ;
502514
515+ const handleCut = useCallback ( ( ) => {
516+ const selectedNodes = nodes . filter ( n => selectedNodeIds . includes ( n . id ) ) ;
517+ if ( selectedNodes . length > 0 ) {
518+ const selectedNodeIdSet = new Set ( selectedNodeIds ) ;
519+ const relatedEdges = edges . filter ( e =>
520+ selectedNodeIdSet . has ( e . source ) && selectedNodeIdSet . has ( e . target )
521+ ) ;
522+ setClipboard ( { nodes : selectedNodes , edges : relatedEdges } ) ;
523+ // Delete the selected nodes
524+ setNodes ( nds => nds . filter ( n => ! selectedNodeIdSet . has ( n . id ) ) ) ;
525+ setEdges ( eds => eds . filter ( e =>
526+ ! selectedNodeIdSet . has ( e . source ) && ! selectedNodeIdSet . has ( e . target )
527+ ) ) ;
528+ setSelectedNodeIds ( [ ] ) ;
529+ setSaved ( false ) ;
530+ }
531+ } , [ nodes , edges , selectedNodeIds , setNodes , setEdges , setSelectedNodeIds , setSaved ] ) ;
532+
533+ const handleCopy = useCallback ( ( ) => {
534+ const selectedNodes = nodes . filter ( n => selectedNodeIds . includes ( n . id ) ) ;
535+ if ( selectedNodes . length > 0 ) {
536+ const selectedNodeIdSet = new Set ( selectedNodeIds ) ;
537+ const relatedEdges = edges . filter ( e =>
538+ selectedNodeIdSet . has ( e . source ) && selectedNodeIdSet . has ( e . target )
539+ ) ;
540+ setClipboard ( { nodes : selectedNodes , edges : relatedEdges } ) ;
541+ }
542+ } , [ nodes , edges , selectedNodeIds ] ) ;
543+
544+ const handlePaste = useCallback ( ( ) => {
545+ if ( ! clipboard ) return ;
546+
547+ // Create a mapping from old node IDs to new node IDs
548+ const idMapping : Record < string , string > = { } ;
549+ let newNextNodeId = nextNodeId ;
550+
551+ const newNodes = clipboard . nodes . map ( node => {
552+ const newId = node . id . startsWith ( 'background-node' )
553+ ? `background-node${ newNextNodeId } `
554+ : `node${ newNextNodeId } ` ;
555+ idMapping [ node . id ] = newId ;
556+ newNextNodeId ++ ;
557+
558+ return {
559+ ...node ,
560+ id : newId ,
561+ position : {
562+ x : node . position . x + 50 ,
563+ y : node . position . y + 50 ,
564+ } ,
565+ } ;
566+ } ) ;
567+
568+ const newEdges = clipboard . edges . map ( ( edge , idx ) => ( {
569+ ...edge ,
570+ id : `e${ Date . now ( ) } -${ idx } ` ,
571+ source : idMapping [ edge . source ] || edge . source ,
572+ target : idMapping [ edge . target ] || edge . target ,
573+ } ) ) ;
574+
575+ setNodes ( nds => [ ...nds , ...newNodes ] ) ;
576+ setEdges ( eds => [ ...eds , ...newEdges ] ) ;
577+ setNextNodeId ( newNextNodeId ) ;
578+ setSelectedNodeIds ( newNodes . map ( n => n . id ) ) ;
579+ setSaved ( false ) ;
580+ } , [ clipboard , nextNodeId , setNodes , setEdges , setNextNodeId , setSelectedNodeIds , setSaved ] ) ;
581+
582+ const handleZoomIn = useCallback ( ( ) => {
583+ zoomIn ( ) ;
584+ } , [ zoomIn ] ) ;
585+
586+ const handleZoomOut = useCallback ( ( ) => {
587+ zoomOut ( ) ;
588+ } , [ zoomOut ] ) ;
589+
590+ const handleResetZoom = useCallback ( ( ) => {
591+ setCenter ( 0 , 0 , { zoom : 1 , duration : 300 } ) ;
592+ } , [ setCenter ] ) ;
593+
594+ const handleFitView = useCallback ( ( ) => {
595+ fitView ( { duration : 300 } ) ;
596+ } , [ fitView ] ) ;
597+
598+ const handleZoomToSelection = useCallback ( ( ) => {
599+ if ( selectedNodeIds . length > 0 ) {
600+ fitView ( { nodes : selectedNodeIds . map ( s => ( { id : s } ) ) , duration : 300 , padding : 0.2 } ) ;
601+ }
602+ } , [ selectedNodeIds , fitView ] ) ;
603+
604+ const handleToggleGrid = useCallback ( ( ) => {
605+ setShowGrid ( prev => ! prev ) ;
606+ } , [ ] ) ;
607+
608+ const handleResetMap = useCallback ( ( ) => {
609+ if ( confirm ( t . resetMapWarning ) ) {
610+ setNodes ( [ ] ) ;
611+ setEdges ( [ ] ) ;
612+ setNextNodeId ( 1 ) ;
613+ setSaved ( false ) ;
614+ }
615+ } , [ setNodes , setEdges , setNextNodeId , setSaved , t ] ) ;
616+
503617 const handleSelectionChange : OnSelectionChangeFunc = useCallback (
504618 ( { nodes : selectedNodes } ) => {
505- if ( selectedNodes . length > 1 ) {
506- setSelectedNodeIds ( selectedNodes . map ( n => n . id ) ) ;
507- } else {
508- setSelectedNodeIds ( [ ] ) ;
509- }
619+ setSelectedNodeIds ( selectedNodes . map ( n => n . id ) ) ;
510620 } ,
511621 [ setSelectedNodeIds ]
512622 ) ;
@@ -543,8 +653,8 @@ export function LearningMapEditor({
543653 e . preventDefault ( ) ;
544654 addNewNode ( "image" ) ;
545655 }
546- // add text node shortcut
547- if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === 'x ' && ! e . shiftKey ) {
656+ // add text node shortcut - changed to Ctrl+T
657+ if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === 'b ' && ! e . shiftKey ) {
548658 e . preventDefault ( ) ;
549659 addNewNode ( "text" ) ;
550660 }
@@ -563,6 +673,60 @@ export function LearningMapEditor({
563673 e . preventDefault ( ) ;
564674 toggleDebugMode ( ) ;
565675 }
676+
677+ // Zoom in shortcut
678+ if ( ( e . ctrlKey || e . metaKey ) && ( e . key === '+' || e . key === '=' ) && ! e . shiftKey ) {
679+ e . preventDefault ( ) ;
680+ handleZoomIn ( ) ;
681+ }
682+ // Zoom out shortcut
683+ if ( ( e . ctrlKey || e . metaKey ) && ( e . key === '-' || e . key === '_' ) && ! e . shiftKey ) {
684+ e . preventDefault ( ) ;
685+ handleZoomOut ( ) ;
686+ }
687+ // Reset zoom shortcut
688+ if ( ( e . ctrlKey || e . metaKey ) && e . key === '0' && ! e . shiftKey ) {
689+ e . preventDefault ( ) ;
690+ handleResetZoom ( ) ;
691+ }
692+ // Fit view shortcut
693+ if ( e . shiftKey && e . code === 'Digit1' && ! e . ctrlKey && ! e . metaKey ) {
694+ e . preventDefault ( ) ;
695+ handleFitView ( ) ;
696+ }
697+ // Zoom to selection shortcut
698+ if ( e . shiftKey && e . code === 'Digit2' && ! e . ctrlKey && ! e . metaKey ) {
699+ e . preventDefault ( ) ;
700+ handleZoomToSelection ( ) ;
701+ }
702+
703+ console . log ( e ) ;
704+ // Toggle grid shortcut
705+ if ( ( e . ctrlKey || e . metaKey ) && e . code === "Backslash" ) {
706+ e . preventDefault ( ) ;
707+ handleToggleGrid ( ) ;
708+ }
709+ // Reset map shortcut
710+ if ( ( e . ctrlKey || e . metaKey ) && e . key === 'Delete' ) {
711+ e . preventDefault ( ) ;
712+ handleResetMap ( ) ;
713+ }
714+ // Cut shortcut
715+ if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === 'x' && ! e . shiftKey ) {
716+ e . preventDefault ( ) ;
717+ handleCut ( ) ;
718+ }
719+ // Copy shortcut
720+ if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === 'c' && ! e . shiftKey ) {
721+ e . preventDefault ( ) ;
722+ handleCopy ( ) ;
723+ }
724+ // Paste shortcut
725+ if ( ( e . ctrlKey || e . metaKey ) && e . key . toLowerCase ( ) === 'v' && ! e . shiftKey ) {
726+ e . preventDefault ( ) ;
727+ handlePaste ( ) ;
728+ }
729+
566730 // Dismiss with Escape
567731 if ( helpOpen && e . key === 'Escape' ) {
568732 setHelpOpen ( false ) ;
@@ -572,7 +736,9 @@ export function LearningMapEditor({
572736 return ( ) => {
573737 window . removeEventListener ( "keydown" , handleKeyDown ) ;
574738 } ;
575- } , [ handleSave , handleUndo , handleRedo , addNewNode , helpOpen , setHelpOpen , togglePreviewMode , toggleDebugMode ] ) ;
739+ } , [ handleSave , handleUndo , handleRedo , addNewNode , helpOpen , setHelpOpen , togglePreviewMode , toggleDebugMode ,
740+ handleZoomIn , handleZoomOut , handleResetZoom , handleFitView , handleZoomToSelection , handleToggleGrid ,
741+ handleResetMap , handleCut , handleCopy , handlePaste ] ) ;
576742
577743 return (
578744 < >
@@ -629,7 +795,7 @@ export function LearningMapEditor({
629795 nodesConnectable = { true }
630796 colorMode = { colorMode }
631797 >
632- < Background />
798+ { showGrid && < Background /> }
633799 < Controls >
634800 < ControlButton title = { t . undo } disabled = { ! canUndo } onClick = { handleUndo } >
635801 < Undo />
0 commit comments