@@ -10,7 +10,12 @@ class TreeView {
1010 this . init ( ) ;
1111 }
1212
13- init ( ) { }
13+ init ( ) {
14+ this . contextMenu = null ;
15+ document . addEventListener ( 'click' , ( e ) => {
16+ this . closeContextMenu ( ) ;
17+ } ) ;
18+ }
1419
1520 /**
1621 * Render JSON data as tree
@@ -97,6 +102,28 @@ class TreeView {
97102 e . stopPropagation ( ) ;
98103 } ) ;
99104
105+ // Right-click context menu
106+ header . addEventListener ( 'contextmenu' , ( e ) => {
107+ e . preventDefault ( ) ;
108+ e . stopPropagation ( ) ;
109+
110+ const jsPath = this . getJsPath ( currentPathArray ) ;
111+ const jsonPointer = this . getJsonPointer ( currentPathArray ) ;
112+
113+ this . showContextMenu ( e . clientX , e . clientY , {
114+ jsPath,
115+ jsonPointer,
116+ value,
117+ key,
118+ pathArray : currentPathArray
119+ } ) ;
120+
121+ this . container
122+ . querySelectorAll ( '.tree-node-header.selected' )
123+ . forEach ( ( el ) => el . classList . remove ( 'selected' ) ) ;
124+ header . classList . add ( 'selected' ) ;
125+ } ) ;
126+
100127 // Toggle arrow for expandable nodes
101128 const toggle = document . createElement ( 'span' ) ;
102129 toggle . className = 'tree-toggle' + ( isExpanded ? ' expanded' : '' ) ;
@@ -252,6 +279,149 @@ class TreeView {
252279 this . render ( ) ;
253280 }
254281
282+ showContextMenu ( x , y , data ) {
283+ this . closeContextMenu ( ) ;
284+
285+ const menu = document . createElement ( 'div' ) ;
286+ menu . className = 'context-menu' ;
287+ menu . style . left = `${ x } px` ;
288+ menu . style . top = `${ y } px` ;
289+
290+ const createItem = ( label , icon , onClick , isDanger = false ) => {
291+ const item = document . createElement ( 'div' ) ;
292+ item . className = `context-menu-item ${ isDanger ? 'danger' : '' } ` ;
293+ item . innerHTML = `<i data-lucide="${ icon } " style="width: 14px; height: 14px;"></i> <span>${ label } </span>` ;
294+ item . addEventListener ( 'click' , ( e ) => {
295+ e . stopPropagation ( ) ;
296+ this . closeContextMenu ( ) ;
297+ onClick ( ) ;
298+ } ) ;
299+ return item ;
300+ } ;
301+
302+ const createDivider = ( ) => {
303+ const divider = document . createElement ( 'div' ) ;
304+ divider . className = 'context-menu-divider' ;
305+ return divider ;
306+ } ;
307+
308+ const copyToClipboard = async ( text , successMsg ) => {
309+ try {
310+ await navigator . clipboard . writeText ( text ) ;
311+ if ( window . App ) App . showToast ( successMsg , 'success' ) ;
312+ } catch ( err ) {
313+ if ( window . App ) App . showToast ( 'Copy failed' , 'error' ) ;
314+ }
315+ } ;
316+
317+ // --- Menu Items ---
318+
319+ // 1. Copy Key (if not root)
320+ if ( data . key !== null && data . key !== undefined && data . key !== '' ) {
321+ menu . appendChild ( createItem ( 'Copy Key' , 'copy' , ( ) => copyToClipboard ( String ( data . key ) , 'Key copied' ) ) ) ;
322+ }
323+
324+ // 2. Copy Value / Content
325+ const valueString = typeof data . value === 'object' ? JSON . stringify ( data . value , null , 2 ) : String ( data . value ) ;
326+ const valueLabel = typeof data . value === 'object' ? 'Copy Content (JSON)' : 'Copy Value' ;
327+ menu . appendChild ( createItem ( valueLabel , 'copy' , ( ) => copyToClipboard ( valueString , 'Content copied' ) ) ) ;
328+
329+ menu . appendChild ( createDivider ( ) ) ;
330+
331+ // 3. Copy Paths
332+ menu . appendChild ( createItem ( 'Copy JS Path' , 'code' , ( ) => copyToClipboard ( data . jsPath , 'JS Path copied' ) ) ) ;
333+ menu . appendChild ( createItem ( 'Copy JSON Pointer' , 'link' , ( ) => copyToClipboard ( data . jsonPointer , 'JSON Pointer copied' ) ) ) ;
334+
335+ menu . appendChild ( createDivider ( ) ) ;
336+
337+ // 4. Extract (Replace content with extracted JSON)
338+ if ( typeof data . value === 'object' ) {
339+ menu . appendChild ( createItem ( 'Extract (Replace content)' , 'external-link' , ( ) => {
340+ if ( window . App && this . container ) {
341+ const editorPanel = this . container . closest ( '.editor-instance' ) ;
342+ if ( editorPanel ) {
343+ const instance = App . editors . find ( e => e . wrapper === editorPanel ) ;
344+ if ( instance ) {
345+ instance . setValue ( valueString ) ;
346+ App . showToast ( 'Extracted JSON into editor' , 'success' ) ;
347+ }
348+ }
349+ } else {
350+ copyToClipboard ( valueString , 'Extracted content copied' ) ;
351+ }
352+ } ) ) ;
353+ }
354+
355+ // 5. Remove Node
356+ // Removing requires updating the actual JSON text and causing a re-render.
357+ // It's safest to mutate the parsed object and text editor if in sync, but here we can just delete from this.data and re-render.
358+ // However, to sync with Monaco text editor, we need to pass the updated JSON up.
359+ if ( data . pathArray . length > 0 ) { // Don't remove root
360+ menu . appendChild ( createItem ( 'Remove Element' , 'trash-2' , ( ) => {
361+ this . removeNodeAtPath ( data . pathArray ) ;
362+ } , true ) ) ;
363+ }
364+
365+ document . body . appendChild ( menu ) ;
366+ this . contextMenu = menu ;
367+
368+ if ( window . lucide ) {
369+ lucide . createIcons ( { root : menu } ) ;
370+ }
371+
372+ // Adjust position if it goes off screen
373+ const rect = menu . getBoundingClientRect ( ) ;
374+ if ( rect . right > window . innerWidth ) {
375+ menu . style . left = `${ window . innerWidth - rect . width - 10 } px` ;
376+ }
377+ if ( rect . bottom > window . innerHeight ) {
378+ menu . style . top = `${ window . innerHeight - rect . height - 10 } px` ;
379+ }
380+ }
381+
382+ closeContextMenu ( ) {
383+ if ( this . contextMenu && this . contextMenu . parentNode ) {
384+ this . contextMenu . parentNode . removeChild ( this . contextMenu ) ;
385+ }
386+ this . contextMenu = null ;
387+ }
388+
389+ removeNodeAtPath ( pathArray ) {
390+ if ( ! pathArray || pathArray . length === 0 ) return ;
391+
392+ // Deep clone data to avoid direct mutation issues, or mutate directly
393+ // Let's mutate directly for simplicity
394+ let current = this . data ;
395+ for ( let i = 0 ; i < pathArray . length - 1 ; i ++ ) {
396+ current = current [ pathArray [ i ] ] ;
397+ }
398+
399+ const lastKey = pathArray [ pathArray . length - 1 ] ;
400+
401+ if ( Array . isArray ( current ) ) {
402+ current . splice ( Number ( lastKey ) , 1 ) ;
403+ } else {
404+ delete current [ lastKey ] ;
405+ }
406+
407+ // Re-render tree
408+ this . render ( ) ;
409+
410+ // Sync with TextEditor if possible
411+ // We dispatch a custom event or check for global App
412+ if ( window . App && this . container ) {
413+ // Hacky way to find parent JsonEditor instance to update its text value
414+ const editorPanel = this . container . closest ( '.editor-instance' ) ;
415+ if ( editorPanel ) {
416+ // App.editors array holds instances
417+ const instance = App . editors . find ( e => e . wrapper === editorPanel ) ;
418+ if ( instance ) {
419+ instance . setValue ( JSON . stringify ( this . data , null , 2 ) ) ;
420+ }
421+ }
422+ }
423+ }
424+
255425 clear ( ) {
256426 this . data = null ;
257427 this . expandedPaths . clear ( ) ;
0 commit comments